-
-
Notifications
You must be signed in to change notification settings - Fork 0
Inject Changes
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.
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) | Specific Gothic version | All | |||
---|---|---|---|---|---|---|---|
Content | Gothic.dat | \Ninja\PatchName\Content | _G1.src | _G112.src | _G130.src | _G2.src | - |
Menu | Menu.dat | \Ninja\PatchName\Menu | _G1.src | _G112.src | _G130.src | _G2.src | .src |
Particle FX | ParticleFX.dat | \Ninja\PatchName\PFX | _G1.src | _G112.src | _G130.src | _G2.src | .src |
Sound FX | SFX.dat | \Ninja\PatchName\SFX | _G1.src | _G112.src | _G130.src | _G2.src | .src |
Visual FX | VisualFX.dat | \Ninja\PatchName\VFX | _G1.src | _G112.src | _G130.src | _G2.src | .src |
Camera | Camera.dat | \Ninja\PatchName\Camera | _G1.src | _G112.src | _G130.src | _G2.src | .src |
Fight AI | Fight.dat | \Ninja\PatchName\Fight | _G1.src | _G112.src | _G130.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 each game 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
, \Ninja\PatchName\Content_G112.src
, \Ninja\PatchName\Content_G130.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.
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.
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.
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.
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.
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.
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[4] = { 5052784, 5120352, 5094928, 5105600 };
if (CALL_Begin(call)) {
const int call = 0;
const int ret = 0;
const int strPtr = 0;
strPtr = _@s(scriptName);
CALL_PtrParam(_@(strPtr));
CALL_PutRetValTo(_@(ret));
CALL__cdecl(zCMenuItem__Create[IDX_EXE]);
call = CALL_End();
};
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) {
var zCMenuItem itmNew;
var zCMenuItem itm;
itm1 = PATCH_[PatchName]_CreateMenuItem(itm1Str);
itm2 = PATCH_[PatchName]_CreateMenuItem(itm2Str);
// Align appearance of left menu item to the existing entry
repeat(index, MEM_ArraySize(items)); var int index;
// Find an existing left column entry
itm = _^(MEM_ArrayRead(items, index));
if (itm.m_parType == MENU_ITEM_TEXT)
&& (itm.m_parItemFlags & IT_EFFECTS_NEXT) {
break;
};
end;
itmNew = _^(itm1);
itmNew.m_parPosX = itm.m_parPosX;
itmNew.m_pFont = itm.m_pFont;
itmNew.m_pFontSel = itm.m_pFontSel;
itmNew.m_parBackPic = itm.m_parBackPic;
// Align appearance of right menu item to the existing entry
repeat(index, MEM_ArraySize(items));
// Find an existing right column entry
itm = _^(MEM_ArrayRead(items, index));
if (itm.m_parType != MENU_ITEM_TEXT)
&& (!(itm.m_parItemFlags & IT_SELECTABLE)) {
break;
};
end;
itmNew = _^(itm2);
itmNew.m_parPosX = itm.m_parPosX;
itmNew.m_pFont = itm.m_pFont;
itmNew.m_pFontSel = itm.m_pFontSel;
itmNew.m_parBackPic = itm.m_parBackPic;
// 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[4] = { 5052784, 5120352, 5094928, 5105600 };
if (CALL_Begin(call)) {
const int call = 0;
const int ret = 0;
const int strPtr = 0;
strPtr = _@s(scriptName);
CALL_PtrParam(_@(strPtr));
CALL_PutRetValTo(_@(ret));
CALL__cdecl(zCMenuItem__Create[IDX_EXE]);
call = CALL_End();
};
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) {
if (!itmPtr) {
return 0;
};
var zCMenuItem itm; itm = _^(itmPtr);
var int fontPtr; fontPtr = itm.m_pFont;
const int zCFont__GetFontY[4] = { /*G1*/7209472, /*G1A*/7440400, /*G2*/7510688, /*G2A*/7902432 };
var int fontHeight;
const int call = 0;
if (CALL_Begin(call)) {
CALL_PutRetValTo(_@(fontHeight));
CALL__thiscall(_@(fontPtr), zCFont__GetFontY[IDX_EXE]);
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[4] = { /*G1*/6267728, /*G1A*/6404400, /*G2*/6427552, /*G2A*/6458144 };
var int valuePtr; valuePtr = _@(value);
const int call = 0;
if (CALL_Begin(call)) {
CALL_IntParam(_@(pos));
CALL_PtrParam(_@(valuePtr));
CALL__thiscall(_@(zCArray_ptr), zCArray__InsertAtPos[IDX_EXE]);
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[4] = { /*G1*/7316336, /*G1A*/7554816, /*G2*/7619584, /*G2A*/8011328 };
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), zCPar_SymbolTable__GetSymbol[IDX_EXE]);
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";
};
// ...
};
This is just one example implementation. An alternative would be to use the above function it replace localized string constants in the menus directly using the following example function.
Click to show code
/*
* Set localized strings in menu scripts
* Adjusted from: https://github.com/szapp/Ninja/wiki/Inject-Changes
*/
func void Patch_[PatchName]_UpdateMenuString(var string symbolName, var string content) {
const int zCPar_SymbolTable__GetSymbol[4] = { 7316336, 7554816, 7619584, 8011328 };
var int symTab; symTab = MEM_ReadInt(menuParserPointerAddress) + 16;
if (CALL_Begin(call)) {
const int call = 0;
const int namePtr = 0;
const int symbPtr = 0;
namePtr = _@s(symbolName);
CALL_PtrParam(_@(namePtr));
CALL_PutRetValTo(_@(symbPtr));
CALL__thiscall(_@(symTab), zCPar_SymbolTable__GetSymbol[IDX_EXE]);
call = CALL_End();
};
if (symbPtr) {
var zCPar_Symbol symb; symb = _^(symbPtr);
MEM_WriteString(symb.content, content);
};
};
Like in the content scripts, it is also important to keep in mind to avoid the usage of common constants in the menu scripts as well.
The script extensions Ikarus and LeGo are handy if not necessary for most script(!) 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_G112.src
and/or Content_G130.src
and/or Content_G2.src
) like so:
Ikarus
LeGo
// any other files
Support for Gothic Sequel and Gothic 2 Classic is provided through the branches gameversions
for each Ikarus and 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.
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.
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. |
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");
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.
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.
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
int NINJA_ID_{PATCHNAME}
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.
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!
};
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"
};
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 Patch_[PatchName]_GetPatchName
and Patch_[PatchName]_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 Patch_[PatchName]_GetPatchNum() { // Adjust name
if (!NINJA_PATCHES) {
return 0;
};
var zCArray arr; arr = _^(NINJA_PATCHES);
return arr.numInArray;
};
func int Patch_[PatchName]_GetPatchObj(var int index) {
if (index >= Patch_[PatchName]_GetPatchNum()) {
MEM_Error("Patch_[PatchName]_GetPatchObj: Index out of bounds!");
return 0;
};
return MEM_ArrayRead(NINJA_PATCHES, index);
};
func string Patch_[PatchName]_GetPatchName(var int index) {
var int patch; patch = Patch_[PatchName]_GetPatchObj(index);
if (patch) {
return STR_FromChar(patch+4);
} else {
return "";
};
};
func string Patch_[PatchName]_GetPatchDescription(var int index) {
var int patch; patch = Patch_[PatchName]_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, Patch_[PatchName]_GetPatchNum()); var int i;
if (Hlp_StrCmp(Patch_[PatchName]_GetPatchName(i), "PATCHNAME")) {
// The patch "PATCHNAME" is installed!
break;
};
end;
This integer constant contains the index of the current patch. It generally does not offer anything and is used internally. Yet, it may be useful in conjunction with NINJA_PATCHES
(see above).
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
};
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.
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
};
Furthermore, this also holds for very standard symbols like classes that are part of every mod. Well, maybe they are not! It is perfectly plausible that a patch may have removed or renamed a class like C_Focus
(in the content scripts) or C_Menu
(in the menu scripts). Therefore, those very common classes need to be redefined under a new name when they are used.
To get a better idea of which symbols might be available and which risk parser errors or incompatibilities, the patch validator is of great help during the development of a patch!
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
*/
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"))
|| (Hlp_StrCmp(panName, "Pánev")) { // 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;
};
};
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 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 | Specific Gothic version | All | |||
---|---|---|---|---|---|---|---|
Humans.mds | \Ninja\PatchName\Anims_ | Humans | _G1.mds | _G112.mds | _G130.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 (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) | Specific Gothic version | All | |||
---|---|---|---|---|---|
\Ninja\PatchName\OU | _G1.bin | _G112.bin | _G130.bin | _G2.bin | .bin |
_G1.csl | _G112.csl | _G130.csl | _G2.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
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