Skip to content

Inject Changes

Sören Zapp edited this page Apr 8, 2024 · 42 revisions

The three types of resources – scripts, animations and output units – differ in their data representation and are completely independent. A patch may, for example, perform changes on the scripts but leave animations and output units untouched. For Ninja to detect what changes should be applied, they are expected in certain format and with specific file names in the virtual directory \Ninja\PatchName\. Any further files that these files refer to may be placed into subdirectories. These details are explained in the sections below.

 

Contents

Daedalus Scripts
    Overwriting Symbols
        Naming Conventions
        Preserved Symbols
    Content 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

 

Daedalus Scripts

Modifying the Daedalus scripts with a patch is a very powerful feature. The scripts in Gothic are divided into different categories, each with their own parser. Likewise they are treated independently by Ninja. Nevertheless, they are injected based on the same principles. After Gothic has loaded the respective scripts from the game or mod (i.e. the DAT file) Ninja looks for a corresponding file for each patch. If found it is parsed immediately after the DAT file. For multiple patches they are parsed in ascending order of their timestamps.

Parser DAT File Expected File Path (Ninja) Gothic 1 Gothic 2 Both
Content Gothic.dat \Ninja\PatchName\Content _G1.src _G2.src -
Menu Menu.dat \Ninja\PatchName\Menu _G1.src _G2.src .src
Particle FX ParticleFX.dat \Ninja\PatchName\PFX _G1.src _G2.src .src
Sound FX SFX.dat \Ninja\PatchName\SFX _G1.src _G2.src .src
Visual FX VisualFX.dat \Ninja\PatchName\VFX _G1.src _G2.src .src
Camera Camera.dat \Ninja\PatchName\Camera _G1.src _G2.src .src
Fight AI Fight.dat \Ninja\PatchName\Fight _G1.src _G2.src .src

The source files work the same way as usual: They list the D files or further SRC files to be parsed with relative paths. However, Ninja does not allow the use of wildcards (? and *) and all consecutive files have to be listed explicitly.

As mentioned previously (Inter-Game Compatibilty), there can be separate files for both games indicated by their file name postfix. A need for this postfix is enforced for the content scripts, i.e. the only valid content sources files are \Ninja\PatchName\Content_G1.src and \Ninja\PatchName\Content_G2.src.

Daedalus symbol indices of patches shift around if the player loads or unloads different patches between saving and loading. Since DAT files are always loaded into the symbol table first, this only affects the symbols introduced by patches, not those of the underlying game or mod. Under normal circumstances this is not an issue. It should be kept in mind not to save symbol indices in variables, but to always refer to symbols by their name.

Overwriting Symbols

Simply parsing the scripts on top of prior loaded DAT files would only allow to add new content, but not modifying existing content. It would also not allow re-adding already existing content (e.g. "Error: Redefined identifier"). While this may seem like a reasonable limitation, it gets problematic when depending on necessary elements that are already present in some mods but not in others (e.g. Ikarus). To make patches work irrespective of the underlying mod, they are allowed to overwrite any existing Daedalus symbol. The different symbol types are treated as follows.

Integer and string constants and variables are replaced. Existing functions are rewritten at a new position in the code stack. The code at their old position is changed to jump to the new position. This is comparable to how Ikarus replaces functions. Classes, prototypes and instances are merged. This means their variables can be changed and they may be extended, but not shrunk. Ninja does not only overwrite the content of a symbol of the same name, but also its properties including its type, number of elements (constants/variables), number of parameters (functions) and so on. Changing the type of an existing symbol will cause issues, as well as increasing the number of function parameters. This is circumvented as described below in Naming Conventions.

While overwriting symbols enables to re-add elements that may or may not be already in the mod, this also opens up possibilities to overwrite and to change content. Nevertheless, overwriting Daedalus symbols and their properties should be done carefully and sparingly. As an example, overwriting the Init_Global (in order to initialize something) would remove any initialization that the underlying mod might have made. Similarly, overwriting the main menu to add one new menu entry would cause any existing, modified menu entries of the mod to get lost. How to implement such changes in a safe way instead is explained below in Content Initialization Functions.

Naming Conventions

To prevent the unintentional overwriting of Daedalus symbols, a patch should avoid common symbol names (e.g. Var1 or a, b, c) and instead choose patch specific names. Patches should adhere to the following naming convention of all its global symbols. (This includes variables, constants, classes, prototypes, instances and functions.)

PatchName_VariableName weak compatibility
Patch_PatchName_VariableName strong compatibility

This is not necessary for local symbols, i.e. symbols defined within the scope of a function, class, prototype or instance, as their internal name is represented as parentName.variableName. If the name of the parent symbol follows the convention, the name of the child symbol will be unique as well.

If the patch name is unique enough, it might suffice as a prefix, i.e. PatchName_VariableName. Nevertheless, including not only the name of the patch, but also a prefix like Patch_ grants higher compatibility and is highly recommended: Say, a mod developer finds and wants to integrate the scripts of a patch directly into their mod. They will be more inclined to change the symbols' names if the names contain words that suggest them being part of a patch. Consequently, a conflict between that mod and the original patch is less likely.

Preserved Symbols

Some symbols will not be overwritten. This includes empty functions and a set of selected symbols that would jeopardize the functionality of the underlying mod. A list of these symbols can be found here. If a patch, however, relies on changing them, this can be done subsequently by setting them from inside a function. Preserved functions can, in turn, be hooked. For more details, see Daedalus Hooks below.

Content Initialization Functions

In order to not only add, but to also change existing content (without replacing it completely), or to trigger, call and use these alterations, there are two initialization functions. These are new, patch-specific functions that are called from Ninja. Each patch can – but does not have to – provide either one or both of these functions and they will be called for all patches in order of their timestamps.

Init_Global

One initialization function is for the content scripts. It is called every time directly after Init_Global and serves as a patch specific equivalent. The expected function signature is

func void Ninja_PatchName_Init()

Since there is no Init_Global in Gothic 1, there the content initialization function is called after the respective Init_[World]. This function may serve to initialized Ikarus and LeGo. How to properly initialize LeGo in a patch is described below in Initializing LeGo.

Menu Creation

A second function is called every time a menu is created, that is, every time when it is opened. It can be used to manipulate the respective menu, e.g. to add a new entry. Additionally, because it is also called just before entering the main menu on game start, it can be used to perform initializations that have to happen before the very first loading or a new game. The expected function signature is

func void Ninja_PatchName_Menu(var int menuPtr)

where menuPtr is a zCMenu pointer to the menu that is being opened.

The simplest way to add a new menu entry with this function is demonstrated below. This example adds a new set of menu items to the game settings just above the "BACK" option. However, this script does not adjust the new menu entries to the existing ones, possibly resulting in an ill-formatted menu. For more a more seamless integration, view the second example.

Click to show code

/*
 * Create menu item from script instance name
 * Source: https://github.com/szapp/Ninja/wiki/Inject-Changes
 */
func int Patch_[PatchName]_CreateMenuItem(var string scriptName) { // Adjust name
    const int zCMenuItem__Create_G1 = 5052784; //0x4D1970
    const int zCMenuItem__Create_G2 = 5105600; //0x4DE7C0

    var int strPtr; strPtr = _@s(scriptName);

    const int call = 0;
    if (CALL_Begin(call)) {
        CALL_PtrParam(_@(strPtr));
        CALL_PutRetValTo(_@(ret));
        CALL__cdecl(MEMINT_SwitchG1G2(zCMenuItem__Create_G1,
                                      zCMenuItem__Create_G2));
        call = CALL_End();
    };

    var int ret;
    return +ret;
};

// ...

/*
 * Menu initialization function called by Ninja every time a menu is opened
 * Source: https://github.com/szapp/Ninja/wiki/Inject-Changes
 */
func void Patch_[PatchName]_Menu(var int menuPtr) {
    MEM_InitAll();

    // Get menu and menu item list, corresponds to C_MENU_DEF.items[]
    var zCMenu menu; menu = _^(menuPtr);
    var int items; items = _@(menu.m_listItems_array);

    // Modify each menu by its name
    if (Hlp_StrCmp(menu.name, "MENU_OPT_GAME")) {

        // New menu instances
        var string itm1Str; itm1Str = "MENUITEM_PATCH_[PATCHNAME]_INST";
        var string itm2Str; itm2Str = "MENUITEM_PATCH_[PATCHNAME]_CHOICE";

        // Get bottom most menu item and new menu items
        var int itmL; itmL = MEM_ArrayPop(items); // Typically "BACK"
        var int itm1; itm1 = MEM_GetMenuItemByString(itm1Str);
        var int itm2; itm2 = MEM_GetMenuItemByString(itm2Str);

        // If the new ones do not exist yet, create them the first time
        if (!itm1) {
            itm1 = PATCH_[PatchName]_CreateMenuItem(itm1Str);
            itm2 = PATCH_[PatchName]_CreateMenuItem(itm2Str);

            // Also adjust vertical positions of the menu items
            var zCMenuItem itm;
            itm = _^(itmL);
            var int y; y = itm.m_parPosY;
            itm.m_parPosY = y+300; // Move bottom item down

            itm = _^(itm1);
            itm.m_parPosY = y-250; // Move new item 1 up

            itm = _^(itm2);
            itm.m_parPosY = y-130; // Move new item 2 up
        };

        // (Re-)insert the menu items in the correct order
        MEM_ArrayInsert(items, itm1);
        MEM_ArrayInsert(items, itm2);
        MEM_ArrayInsert(items, itmL);
    };

    /* Modify other menus as well:
     .. else if (Hlp_StrCmp(menu.name, "XXXX")) {

        // ...

    }; */
};

To integrate new menu entries more seamlessly, more elaborate code is necessary. The example below shows how to add a new entry to the key bindings menu while mimicking the existing entries in position, font and size.

Click to show code

/*
 * Create menu item from script instance name
 * Source: https://github.com/szapp/Ninja/wiki/Inject-Changes
 */
func int Patch_[PatchName]_CreateMenuItem(var string scriptName) { // Adjust name
    const int zCMenuItem__Create_G1 = 5052784; //0x4D1970
    const int zCMenuItem__Create_G2 = 5105600; //0x4DE7C0

    var int strPtr; strPtr = _@s(scriptName);

    const int call = 0;
    if (CALL_Begin(call)) {
        CALL_PtrParam(_@(strPtr));
        CALL_PutRetValTo(_@(ret));
        CALL__cdecl(MEMINT_SwitchG1G2(zCMenuItem__Create_G1,
                                      zCMenuItem__Create_G2));
        call = CALL_End();
    };

    var int ret;
    return +ret;
};

/*
 * Copy essential properties from one to another menu entry
 * Source: https://github.com/szapp/Ninja/wiki/Inject-Changes
 */
func void Patch_[PatchName]_CopyMenuItemProperties(var int dstPtr, var int srcPtr) {
    if (!dstPtr) || (!srcPtr) {
        return;
    };

    var zCMenuItem src; src = _^(srcPtr);
    var zCMenuItem dst; dst = _^(dstPtr);

    dst.m_parPosX = src.m_parPosX;
    dst.m_parPosY = src.m_parPosY;
    dst.m_parDimX = src.m_parDimX;
    dst.m_parDimY = src.m_parDimY;
    dst.m_pFont = src.m_pFont;
    dst.m_pFontSel = src.m_pFontSel;
    dst.m_parBackPic = src.m_parBackPic;
};

/*
 * Get maximum menu item height
 * Source: https://github.com/szapp/Ninja/wiki/Inject-Changes
 */
func int Patch_[PatchName]_MenuItemGetHeight(var int itmPtr) { // Adjust name
    if (!itmPtr) {
        return 0;
    };

    var zCMenuItem itm; itm = _^(itmPtr);
    var int fontPtr; fontPtr = itm.m_pFont;

    const int zCFont__GetFontY_G1 = 7209472; //0x6E0200
    const int zCFont__GetFontY_G2 = 7902432; //0x7894E0

    var int fontHeight;
    const int call = 0;
    if (CALL_Begin(call)) {
        CALL_PutRetValTo(_@(fontHeight));
        CALL__thiscall(_@(fontPtr), MEMINT_SwitchG1G2(zCFont__GetFontY_G1,
                                                      zCFont__GetFontY_G2));
        call = CALL_End();
    };

    // Transform to virtual pixels
    MEM_InitGlobalInst();
    var zCView screen; screen = _^(MEM_Game._zCSession_viewport);
    fontHeight *= 8192 / screen.psizey;

    if (fontHeight > itm.m_parDimY) {
        return fontHeight;
    } else {
        return itm.m_parDimY;
    };
};

/*
 * Insert value into array at specific position
 * Source: https://github.com/szapp/Ninja/wiki/Inject-Changes
 */
func void Patch_[PatchName]_ArrayInsertAtPos(var int zCArray_ptr,
                                             var int pos,
                                             var int value) { // Adjust name
    const int zCArray__InsertAtPos_G1 = 6267728; //0x5FA350
    const int zCArray__InsertAtPos_G2 = 6458144; //0x628B20

    var int valuePtr; valuePtr = _@(value);

    const int call = 0;
    if (CALL_Begin(call)) {
        CALL_IntParam(_@(pos));
        CALL_PtrParam(_@(valuePtr));
        CALL__thiscall(_@(zCArray_ptr), MEMINT_SwitchG1G2(zCArray__InsertAtPos_G1,
                                                          zCArray__InsertAtPos_G2));
        call = CALL_End();
    };
};


// ...


/*
 * Menu initialization function called by Ninja every time a menu is opened
 * Source: https://github.com/szapp/Ninja/wiki/Inject-Changes
 */
func void Patch_[PatchName]_Menu(var int menuPtr) { // Adjust name
    MEM_InitAll();

    // Get menu and menu item list, corresponds to C_MENU_DEF.items[]
    var zCMenu menu; menu = _^(menuPtr);
    var int items; items = _@(menu.m_listItems_array);

    // Modify each menu by its name
    if (Hlp_StrCmp(menu.name, "MENU_OPT_CONTROLS")) {

        // New menu instances (description and key binding)
        var string itm1Str; itm1Str = "MENUITEM_KEY_PATCH_[PATCHNAME]";
        var string itm2Str; itm2Str = "MENUITEM_INP_PATCH_[PATCHNAME]";

        // Get new items
        var int itm1; itm1 = MEM_GetMenuItemByString(itm1Str);
        var int itm2; itm2 = MEM_GetMenuItemByString(itm2Str);

        // If the new ones do not exist yet, create them the first time
        if (!itm1) {
            var zCMenuItem itm;
            itm1 = Patch_[PatchName]_CreateMenuItem(itm1Str);
            itm2 = Patch_[PatchName]_CreateMenuItem(itm2Str);

            // Copy properties of first key binding entry (left column)
            var int itmF_left; itmF_left = MEM_ArrayRead(items, 1);
            Patch_[PatchName]_CopyMenuItemProperties(itm1, itmF_left);
            itm = _^(itmF_left);
            var int ypos_l; ypos_l = itm.m_parPosY;

            // Retrieve right column entry and copy its properties too
            var string rightname; rightname = itm.m_parOnSelAction_S;
            rightname = STR_SubStr(rightname, 4, STR_Len(rightname)-4);
            var int itmF_right; itmF_right = MEM_GetMenuItemByString(rightname);
            if (itmF_right) {
                Patch_[PatchName]_CopyMenuItemProperties(itm2, itmF_right);
            } else { // If not found, copy from left column
                Patch_[PatchName]_CopyMenuItemProperties(itm2, itmF_left);
                itm = _^(itm2);
                itm.m_parPosX += 2700; // Default x position
            };
            itm = _^(itmF_right);
            var int ypos_r; ypos_r = itm.m_parPosY;

            // Find "BACK" menu item by its action (to add the new ones above)
            const int index = 0;
            repeat(index, MEM_ArraySize(items));
                itm = _^(MEM_ArrayRead(items, index));
                if (itm.m_parOnSelAction == /*SEL_ACTION_BACK*/ 1)
                && (itm.m_parItemFlags & /*IT_SELECTABLE*/ 4) {
                    break;
                };
            end;
            var int y; y = itm.m_parPosY; // Obtain vertical position

            // Adjust height of new entries (just above the "BACK" option)
            itm = _^(itm1);
            itm.m_parPosY = y;
            itm = _^(itm2);
            itm.m_parPosY = y + (ypos_r - ypos_l); // Maintain possible difference

            // Get maximum height of new entries
            var int ystep; ystep = Patch_[PatchName]_MenuItemGetHeight(itm1);
            var int ystep_r; ystep_r = Patch_[PatchName]_MenuItemGetHeight(itm2);
            if (ystep_r > ystep) {
                ystep = ystep_r;
            };

            // Shift vertical positions of all following menu items below
            repeat(i, MEM_ArraySize(items) - index); var int i;
                itm = _^(MEM_ArrayRead(items, i + index));
                itm.m_parPosY += ystep;
            end;
        };

        // Add new entries at the correct position
        Patch_[PatchName]_ArrayInsertAtPos(items, index, itm1);
        Patch_[PatchName]_ArrayInsertAtPos(items, index+1, itm2);
    };

    /* Modify other menus as well:
     .. else if (Hlp_StrCmp(menu.name, "XXXX")) {

        // ...

    }; */
};

To support localization a constant can be placed in the menu scripts

const int Patch_[PatchName]_Lang = 0; // Will be set automatically

that may be adjusted from the menu initialization function. The below script requires the language function as explained in Localization.

Click to show code

/*
 * Set localization indicator in menu scripts
 * Source: https://github.com/szapp/Ninja/wiki/Inject-Changes
 */
func void Patch_[PatchName]_SetMenuLocalization() { // Adjust name
    const int zCPar_SymbolTable__GetSymbol_G1 = 7316336; //0x6FA370
    const int zCPar_SymbolTable__GetSymbol_G2 = 8011328; //0x7A3E40

    var string symbolName; symbolName = "Patch_[PatchName]_Lang"; // Adjust name
    var int symTab; symTab = MEM_ReadInt(menuParserPointerAddress) + 16;
    var int namePtr; namePtr = _@s(symbolName);

    const int call = 0;
    if (CALL_Begin(call)) {
        CALL_PtrParam(_@(namePtr));
        CALL_PutRetValTo(_@(symbPtr));
        CALL__thiscall(_@(symTab), MEMINT_SwitchG1G2(zCPar_SymbolTable__GetSymbol_G1, zCPar_SymbolTable__GetSymbol_G2));
        call = CALL_End();
    };

    var int symbPtr;
    if (symbPtr) {
        var zCPar_Symbol symb; symb = _^(symbPtr);
        symb.content = Patch_[PatchName]_GuessLocalization(); // Adjust name
    };
};

Within the individual menu instances, this constant aids in setting the strings according to the detected lanuage. Here an example

const int Patch_[PatchName]_Lang = 0; // Will be set automatically


instance MenuItem_Patch_[PatchName]_Menu(C_MENU_ITEM_DEF) {
    // Set the text display according to the language
    if (Patch_[PatchName]_Lang == 1) { // DE (Windows 1252)
        text[0] = "Das ist deutsch";
    } else if (Patch_[PatchName]_Lang == 2) { // PL (Windows 1250)
        text[0] = "To jest polski";
    } else if (Patch_[PatchName]_Lang == 3) { // RU (Windows 1251)
        text[0] = "Это русский";
    } else { // EN
        text[0] = "This is English";
    };

    // ...
};

Like in the content scripts, it is also important to keep in mind avoid usage of constants in the menu scripts as well.

Ikarus and LeGo

The script extensions Ikarus and LeGo are handy if not necessary for most patches. Since LeGo is under active development, using it in a patch poses a severe risk of incompatibility to other patches and the underlying mod. A mod that contains a newer version than that of the patch would cause issues, because the older version from the patch would overwrite the newer version of the mod. To prevent such incompatibilities, Ninja not only compares and verifies the versions, but also ships with always the latest versions of both Ikarus and LeGo. These versions are also adjusted to work better with patches, as described below in Modifications to LeGo.

This should, however, not be misunderstood: Ninja merely supplies these scripts, but does not apply them. Only if a patch requires Ikarus or LeGo, they will be parsed. Without any patch active, Ninja does not perform any changes by itself.

To enforce this compatibility, patches are consequently forbidden from containing Ikarus and LeGo themselves. To use these script packages, their names merely have to be added to the content source file (Content_G1.src and/or Content_G2.src) like so:

Ikarus
LeGo

// any other files

Initializing LeGo

The packages offered by LeGo require prior and continuous (i.e. on every loading) initialization. Since subsequent calls to LeGo_Init are ignored or may even do harm, Ninja supplies a wrapper function that merges prior initialization with new initializations of packages. If it is necessary, this function should be called from the Content Initialization Functions, see above.

LeGo_MergeFlags(var int flags)

Its usage is equivalent to LeGo_Init. Forbidden flags are LeGo_All, as a patch should only initialize what it really needs, and a few others listed in PermMem and Handles.

Modifications to LeGo

As of Version 2.2.02 PermMem handles are no longer skipped on saving. More details in PermMem and Handles. There are only few other technical adjustments to LeGo.

Any handle-dependent feature should be used sparingly if not omitted wherever possible.

PermMem and Handles

Since Ninja 2.2.02 LeGo-PermMem handles are persistent across saving and loading. This allows even more powerful script features. Created handles are stored in the same fashion as PermMem, but in patch-specific files separate from the files of the mod. This way, removal of a patch that introduces handles no longer leads to problems. Aside from LeGo classes, a patch may even introduce new classes and save handles based on them without issues.

For patches using this functionality it is very important to include a version check of NINJA_VERSION against 2202 to ensure that at least version 2.2.02 of Ninja is installed.

Nevertheless, there remain some technical limitations for handles created through LeGo as part of certain LeGo packages. The followning packages are not allowed for use in patches.

Package Access Reason/Notes
Buffs If the mod or any other patch was using buffs, buffs in a patch would lead to undefined behavior once the patch was removed.
Talents It cannot be safely established which one is the next free AI-variable.
Names It relies on the package Talents (see above).
Gamestate Since there can only be one gamestate event, gamestate listeners from patches cannot be ensured to work across mod and patches and are not guaranteed to be saved (see EventHandler). Alternatives are very easy to implement (e.g. using FrameFunctions).
EventHandler strongly discouraged If the event and its listeners do not both originate from the mod or the same patch, the respective listener will not be archived at all, i.e. it will be lost on saving and loading. Events will only work properly if both the event and all its listeners are created from the identical patch.
Focusnames not recommended The function _Focusnames is non-overwritable. In order to set coloring of focus names, the function can be hooked instead, using Daedalus Hooks. Very careful coding and a lot of foresight is required to not interfere with the scripts of the underlying mod and other patches.
Saves   The functions BW_Savegame and BR_Savegame are non-overwritable. In order to run code on saving/loading, the the engine function these function are hooking can be hooked instead.
User Constants   LeGo's user constants (Userconst.d) are non-overwritable. These are dictated by the mod.

Daedalus Hooks

As described above in Preserved Symbols, Ninja does not allow to overwrite specific functions. It is encouraged to hook before or after these functions instead to preserve their original functionality. LeGo offers documentation on how to register such hooks here and here. Using the string-parameter function is recommended to prevent an error if the original function does not exist, e.g.

HookDaedalusFuncS("function", "Patch_PatchName_function");

Inserting NPC

NPC that are stored in a game save but do not exist in the scripts when loading cause the game to crash. To prevent this from happening when removing a patch from the game that inserted an NPC, Ninja does by default not archive any NPC of a patch-specific instance. More technically, when an NPC that is defined by a patch is inserted, Ninja will flag it as dontWriteIntoArchive. A patch must therefore deal with the non-persistence of NPC.

Nevertheless, depending on the type of patch, this flag may be removed to add the NPC to the game save. An example might be a patch introducing a new story line with new characters and quests. Removal of the patch is will still be possible without compromising the game save, as described in Other Mechanics. The flag is therefore just an additional cautionary measure by Ninja.

Disallow Saving

More elaborate patches might add a new world or similar. It should be noted that in such cases game saves will no longer work when removing the patch. This is due to the game loading a world that will then no longer exist. These aspects should always be kept in mind and the developer of the patch is expected to extend a lot of foresight about all the changes they introduce.

If the gameplay that takes place in the new world is expected to be rather short, the developer could include the EnforceSavingPolicy script to prevent the player from creating game saves during that portion of the game. Some of its functions are included in the preserved symbols list, and should be hooked instead, see Preserved Symbols and Daedalus Hooks.

Click to show code

/*
 * Hook saving policy (Ninja does not allow overwriting the saving policy functions,
 * only hooking them)
 * This function is to be called from Ninja_[PatchName]_Init after proper
 * initialization of Ikarus and LeGo
 */
func void Patch_[PatchName]_SavingPolicyInit() {
    HookDaedalusFunc(AllowSaving, Patch_[PatchName]_SavingPolicy);
};

/*
 * Disallow saving when in the unknown world (hooks AllowSaving)
 */
func int Patch_[PatchName]_SavingPolicy() {
    if (Patch_[PatchName]_Quest == Patch_[PatchName]_InWorld) {
        // If the player is currently in the new world, disallow saving by all means
        return FALSE;
    } else {
        // Otherwise continue with the original saving policy of the mod (if present)
        ContinueCall();
    };
};

Alternatively, or if the period in the new world is expected to be longer resulting in a complete new story line, the patch should be advertised as such and educate the player thoroughly that removal of the patch is not possible once added.

Helper Symbols

Ninja introduces a few additional Daedalus symbols with further information that can be handy to patch developers.

int NINJA_VERSION
string NINJA_MODNAME
zCArray* NINJA_PATCHES
instance NINJA_SYMBOLS_START
instance NINJA_SYMBOLS_START_{PATCHNAME}
instance NINJA_SYMBOLS_END_{PATCHNAME}

These symbols will be created by Ninja and do not have to exist in the scripts beforehand. If they already exist, they will be appropriately overwritten and filled.

NINJA_VERSION

The version of the current instance of Ninja can be dynamically read from the constant NINJA_VERSION. This is an integer combining the base (one digit), major (one digit) and minor (two digits) version of Ninja. For Ninja version 2.0.01 this integer will be 2001. This may be useful to make sure that certain features of Ninja are available based on the version number.

if (NINJA_VERSION < 2000) {
    MEM_SendToSpy(zERR_TYPE_FATAL, "Please update Ninja to version 2.0 or higher.");
};

This symbol is also useful for mods to infer if Ninja is installed. By creating a stub constant in the mod scripts

const int NINJA_VERSION = 0;

and later checking if it has been filled with a different value allows to check for Ninja.

const int NINJA_VERSION = 0;

// ...

if (NINJA_VERSION) {
    // Ninja is installed!
};

NINJA_MODNAME

Likewise a patch may restrict itself or parts of its features to certain mods. This can be easily checked without complicated extractions with

if (Hlp_StrCmp(NINJA_MODNAME, "GOTHICGAME")) {
    // The original game is running! Apply an exclusive feature here
};

This can also be used to deal with incompatibilities with certain mods and may act as a patch-side complementary feature to Ninja's Incompatibility List for Mods.

if (!Hlp_StrCmp(NINJA_MODNAME, "SOMEMOD")) {
    // A feature is already included in "SOMEMOD"
};

NINJA_PATCHES

Furthermore, patches (as well as mods) are granted insight into all loaded patches easily. The integer constant NINJA_PATCHES contains a pointer to an zCArray. Each element corresponds to one patch and has a size of 548 bytes with the following structure:

Size Type Description
4 bytes int Timestamp
288 bytes char Patchname
256 bytes char Description

How to access these fields is best illustrated by the following code with Ninja_GetPatchName and Ninja_GetPatchDescription.

Click to show code

/*
 * Functions for retrieving information about loaded patches
 * Source: https://github.com/szapp/Ninja/wiki/Inject-Changes#helper-symbols
 */

func int Ninja_GetPatchNum() {
    if (!NINJA_PATCHES) {
        return 0;
    };
    var zCArray arr; arr = _^(NINJA_PATCHES);
    return arr.numInArray;
};

func int Ninja_GetPatchObj(var int index) {
    if (index >= Ninja_GetPatchNum()) {
        MEM_Error("Ninja_GetPatchObj: Index out of bounds!");
        return 0;
    };
    return MEM_ArrayRead(NINJA_PATCHES, index);
};

func string Ninja_GetPatchName(var int index) {
    var int patch; patch = Ninja_GetPatchObj(index);
    if (patch) {
        return STR_FromChar(patch+4);
    } else {
        return "";
    };
};

func string Ninja_GetPatchDescription(var int index) {
    var int patch; patch = Ninja_GetPatchObj(index);
    if (patch) {
        return STR_FromChar(patch+4+288);
    } else {
        return "";
    };
};

This information can also be used from inside a mod project. As demonstrated for the symbol NINJA_VERSION above, a mod can create a stub constant in the mod scripts

const int NINJA_PATCHES = 0;

and later check if it has been filled with a pointer to a zCArray.

const int NINJA_PATCHES = 0;

// ...

repeat(i, Ninja_GetPatchNum()); var int i;
    if (Hlp_StrCmp(Ninja_GetPatchName(i), "PATCHNAME")) {
        // The patch "PATCHNAME" is installed!
        break;
    };
end;

NINJA_SYMBOLS_START

To differentiate between Daedalus symbols present in the underlying mod (DAT file) and the ones introduced by patches, Ninja adds a divider symbol to the symbol tables before adding any symbols from the patches. Internally this is used to add exceptions for patch specific symbols.

Likewise, a patch can make use of this divider symbol. Since this symbol is an instance, the comparison by symbol index is very convenient.

var int id; id = MEM_GetSymbolIndex("Variable");
if (id < NINJA_SYMBOLS_START) {
    // Symbol was introduced by the mod
} else {
    // Symobl is part of a patch
};

NINJA_SYMBOLS_START/END_PATCHNAME

Since Ninja 2.1.01 Daedalus symbols can further be differentiated. Additional divider symbols describe the lower and upper bounds for each loaded patch and allow to find the patch which introduced a symbol. This works analogous as demonstrated in NINJA_SYMBOLS_START.

var int id; id = MEM_GetSymbolIndex("Variable");
if (NINJA_SYMBOLS_START_MYPATCH < id) && (id < MEM_FindParserSymbol("NINJA_SYMBOLS_END_MYPATCH")) {
    // Symbol was introduced by the patch called MYPATCH
};

Note that NINJA_SYMBOLS_END_MYPATCH cannot be referenced directly, because it is parsed after the patch and the symbol does not exist at this point yet. Therefore, it is wrapped into MEM_FindParserSymbol here.

Common Symbols

It is very important to mention that a patch should never presuppose any, not even the most commonly used, variables (or symbols in general). A good example is AI variables of NPC. These are present in the original games and will mostly be used in the same way in mods. This assumption is simply too weak. Mods can and will rename and repurpose those variables, as well as any other symbol, of the original scripts. Every time a patch references a symbol that "should" exist in the mod, it is always better to refer to it only by its name in a string (to prevent parser errors, i.e. "Unknown Identifier") and check for its existence with the Ikarus function MEM_FindParserSymbol. If confirmed, its content can be read from zCPar_Symbol.content and if not, a default value should be readily available.

var int value;
if (MEM_FindParserSymbol("Variable") != -1) {
    var zCPar_Symbol symb; symb = _^(MEM_GetSymbol("Variable"));
    value = symb.content;
} else {
    value = 10; // Default value if the variable does not exist
};

Localization

As well as patches are inter-game compatible, they can also be easily designed to be multi-lingual, which is highly encouraged. This involves strings in content and menu scripts only. String constants can be adjusted according to language with suitable if-conditions in the initialization function, see Content Initialization Functions. This may look like this.

Click to show code

Note: The characters in this code snippet are already encoded, copy and save it with encoding 'Windows 1252'

/*
 * Guess localization
 * Indices are assigned in no particular order (new ones added at the end)
 * Source: https://github.com/szapp/Ninja/wiki/Inject-Changes#localization
 *
 * EN =  0 (default)
 * DE =  1
 * PL =  2
 * RU =  3
 * IT =  4
 * ES =  5
 * FR =  6
 * CS =  7
 * HU =  8
 * RO =  9
 * UK = 10
 * TR = 11
 * CY = 12
 * ZH = 13
 */
func int Patch_[PatchName]_GuessLocalization() {
    var int pan; pan = MEM_GetSymbol("MOBNAME_PAN");
    if (pan) {
        var zCPar_Symbol panSymb; panSymb = _^(pan);
        var string panName; panName = MEM_ReadString(panSymb.content);
        if (Hlp_StrCmp(panName, "Pfanne")) {              // DE cp1252
            return 1;
        } else if (Hlp_StrCmp(panName, "Patelnia")) {     // PL cp1250
            return 2;
        } else if (Hlp_StrCmp(panName, "Ñêîâîðîäà")) {    // RU cp1251
            return 3;
        } else if (Hlp_StrCmp(panName, "Padella")) {      // IT cp1252
            return 4;
        } else if (Hlp_StrCmp(panName, "Sartén")) {       // ES cp1252
            return 5;
        } else if (Hlp_StrCmp(panName, "Casserole")) {    // FR cp1252
            return 6;
        } else if (Hlp_StrCmp(panName, "Pánvièka")) {     // CS cp1250
            return 7;
        } else if (Hlp_StrCmp(panName, "Serpenyõ")) {     // HU cp1250
            return 8;
        } else if (Hlp_StrCmp(panName, "Tigaie")) {       // RO cp1250
            return 9;
        } else if (Hlp_StrCmp(panName, "Ïàòåëüíÿ")) {     // UK cp1251
            return 10;
        } else if (Hlp_StrCmp(panName, "Tava")) {         // TR cp1254
            return 11;
        } else if (Hlp_StrCmp(panName, "Padell")) {       // CY
            return 12;
        } else if (Hlp_StrCmp(panName, "平底锅"))
               || (Hlp_StrCmp(panName, "�")) {     // ZH
            return 13;
        };
    };
    return 0; // Otherwise EN
};

// ...

const string Patch_[PatchName]_SomeText = "This is English";

// ...

/*
 * Function called from content or menu initialization function
 * Source: https://github.com/szapp/Ninja/wiki/Inject-Changes#localization
 */
func void Patch_[PatchName]_LocalizeTexts() {
    var int lang; lang = Patch_[PatchName]_GuessLocalization();
    if (lang == 1) // DE (Windows 1252)
    {
        Patch_[PatchName]_SomeText = "Das ist deutsch";
    }
    else if (lang == 2) // PL (Windows 1250)
    {
        Patch_[PatchName]_SomeText = "To jest polski";
    }
    else if (lang == 3) // RU (Windows 1251)
    {
        Patch_[PatchName]_SomeText = "Ýòî ðóññêèé";
    };
    // { ... }
    // Else: Keep default -> English
};

Animations and Armor

Animations and armor are collectively "registered" in the MDS files. (In Gothic 2 NotR these are additionally complied into MSB files. However, this detail is not important here.)

Whenever Gothic is loading a model (zCModelPrototype) from its MDS/MSB file, Ninja looks for a file matching the model name and parses it directly afterwards. This means these changes are only applied once a model is loaded during the game. To replace or add new animations or register new armor, an MDS file corresponding to the respective model is required. The model is specified by the file name.

Corresponds to Expected File Path (Ninja) Model Gothic 1 Gothic 2 Both
Humans.mds \Ninja\PatchName\Anims_ Humans _G1.mds _G2.mds .mds
Bloodfly.mds Bloodfly
Gobbo.mds Gobbo
  ...

The preference of files by their postfix is analogous to Daedalus Scripts above.

The files follow the same syntax of the MDS format. However, aside from the structure, only the new entries have to be included.

// Model descriptor, e.g. "HuS" for Humans
Model ("HuS")
{
    // NEW ARMOR HERE

    aniEnum
    {
        // NEW ANIMATIONS HERE
    }
}

Individual animation files referenced in the MDS must be provided in complied form in the usual directory, i.e. \_work\Data\Anims\_compiled\.

It is possible to overwrite properties of existing animations (reverse flag, event block, etc.), but not the file name to existing animations themselves (will be ignored silently). In order to effectively overwrite an animation, the specific compiled animation file itself should be replaced.

It is important to mention that Ninja prevents writing of MSB binary files. Ninja should not be used in a mod-kit installation of Gothic 2 NotR when expecting to compile new animations. (Gothic 1 is not affected by this.)

Output Units

Output units (i.e. dialog lines) are collectively stored in a single file OU.bin or OU.csl. With Ninja new output units may be added as well as existing ones overwritten by supplying either one of these files containing the altered or new blocks. The file structure must be valid and it is encouraged to have the file generated from the scripts with Gothic Spacer or tools like Redefix (as done usually).

As with normal loading of output units, the BIN file takes priority. If (and only if) no BIN file is found, Ninja looks for the CSL file. For the sake of transparency it is encouraged to always use CSL files, because they can be more easily altered manually.

Expected File Path (Ninja) Gothic 1 Gothic 2 Both
\Ninja\PatchName\OU _G1.bin _G2.bin .bin
  _G1.csl _G1.csl .csl

The preference of files by their postfix is analogous to Daedalus Scripts above.

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