Skip to content

Reduce Projectiles

szapp edited this page Nov 3, 2017 · 2 revisions

Incorporating the Re-usable Projectile feature (GFA_REUSE_PROJECTILES) into an existing mod-project introduces balancing issues, where the number of available arrows and bolts increases as they may be picked up and re-used. The player will have way to many projectiles at their disposal. This not only makes this feature completely useless but also leads to inflation, because vast amounts of projectiles can be sold to traders and the player becomes richer.

To counterbalance this, the overall number of existing projectiles in the world, chests and inventories of NPCs need to be reduced immensely. While this adjustment is not much effort script-wise, the changes to the world are exhausting (a lot of time consuming Spacer work). This work can be replaced by a few lines of script code that is provided here.

At first initialization of each world a script will search for all arrows and bolts laying in the world, as well as iterate over all containers and their contents. It will then remove some of the projectiles, leaving only an adjustable fraction of them remaining.

This wiki page will provide this script and explain it in three parts.
 

Contents

1.   NPC Inventories
2.   Chests and Containers
3.   World
4.   Complete Script


NPC Inventories

The number of projectiles in the inventories of all NPCs can be reduced by adjusting their NPC instances in the scripts as well as the trading scripts. This has two disadvantages:

  • It takes a lot of time to go through all scripts to adjust these numbers. In case the fraction of remaining projectiles turns out to be still too many or too few, this process has to be repeated.
  • Should this be a patch to an already released modification, any player would have to start a new game, because the NPCs will have already spawned at that time. Likewise any later patch or update on the modification cannot take action on the number of projectiles.

Instead, the recommended approach is to iterate over all NPCs and their inventory on initialization of each world. This way, any game in progress will also be affected and this process can be repeated should the number of projectiles be adjusted in later patches. This method also has a disadvantage to it you should be aware of.

  • This reduction of iteration only takes place once and will only affect NPCs that are in the respective world from the beginning (typically via Wld_InsertNpc in the startup function of the world). Any NPC added at a later point, from a dialog or with the start of a new chapter, will not be affected. Those, usally only few, NPCs should still be adjusted in their NPC instances.

The only thing left to do is to adjust the trade inventories if your mod has them separate as done in the vanilla Gothic 2. Do not forget the scripts that are triggered at the start of each chapter to distribute new goods to all traders.

Requirements

To use this script you will need the Broadcast scripts written by Sektenspinner available here.

The Script

Explanation on how to use the function is below.

/* Fraction of remaining projectiles, e.g 0.2 to remove 80% of the projectiles  */
var float _projectileReduceToFraction;

/*
 * Broadcast function to reduce number of arrows and bolts from all (N)PCs
 */
func void reduceItemsInInventory(var C_Npc slf) {
    var string instName; instName = MEM_ReadString(MEM_GetSymbolByIndex(Hlp_GetInstanceID(slf)));

    // Get arrow and bolt instances depending on G1 or G2
    var int arrowInst;
    var int boltInst;
    if (GOTHIC_BASE_VERSION == 1) {
        arrowInst = MEM_GetSymbolIndex("ItAmArrow");
        boltInst  = MEM_GetSymbolIndex("ItAmBolt");
    } else {
        arrowInst = MEM_GetSymbolIndex("ItRw_Arrow");
        boltInst  = MEM_GetSymbolIndex("ItRw_Bolt");
    };

    // Number of projectiles present in the inventory
    var int numArrows; numArrows = Npc_HasItems(slf, arrowInst);
    var int numBolts;  numBolts  = Npc_HasItems(slf, boltInst);
    var int reduceTo;
    var int s;

    // Reduce for player only once ever!
    var int gotPlayer;
    if (Npc_IsPlayer(slf)) {
        if (gotPlayer) {
            return;
        };
        gotPlayer = TRUE;
    };

    // Reduce number of arrows
    if (numArrows) {
        reduceTo = roundf(mulf(mkf(numArrows), castToIntf(_projectileReduceToFraction))); // Amount * x

        // Keep at least two instances
        if (reduceTo < 2) {
            reduceTo = 2;
        };
        Npc_RemoveInvItems(slf, arrowInst, numArrows-reduceTo);

        s = SB_New();
        SB("  Removed ");
        SBi(numArrows-reduceTo);
        SB(" of ");
        SBi(numArrows);
        SB(" arrows from ");
        SB(instName);
        SB(", kept ");
        SBi(reduceTo);

        MEM_Info(SB_ToString());
        SB_Destroy();
    };

    // Reduce number of bolts
    if (numBolts) {
        reduceTo = roundf(mulf(mkf(numBolts), castToIntf(_projectileReduceToFraction))); // Amount * x

        // Keep at least two instances
        if (reduceTo < 2) {
            reduceTo = 2;
        };
        Npc_RemoveInvItems(slf, boltInst, numBolts-reduceTo);

        s = SB_New();
        SB("  Removed ");
        SBi(numBolts-reduceTo);
        SB(" of ");
        SBi(numBolts);
        SB(" bolts from ");
        SB(instName);
        SB(", kept ");
        SBi(reduceTo);

        MEM_Info(SB_ToString());
        SB_Destroy();
    };
};

The function reduceItemsInInventory will be called for all NPCs including the player when called with

_projectileReduceToFraction = 0.2;
DoForAll(reduceItemsInInventory);

The variable _projectileReduceToFraction should be set to the desired fraction of remaining projectiles. Recommended is 0.2 (20% of the projectiles will remain). This might sound like it is way too little, but this value was tested thoroughly.

This call should be performed at the end of the initialization of each world - only once for each world:

func void Init_World1()
{
    // ...

    var int reduceOnce;
    if (!reduceOnce) {
        MEM_Info("Reduce the number of projectiles in this world (once).");

        // NPC inventories
        _projectileReduceToFraction = 0.2;
        DoForAll(reduceItemsInInventory);

        reduceOnce = TRUE;
    };
};

Additionally, it is recommended to remove any trace of the projectiles from the NPC instance scripts. The engine automatically creates the respective munition for an NPC the first time they are initialized if they have a ranged weapon. In the scope of the Re-usable Projectile feature this number of automatically created projectiles has been adjusted to five (by default 50).

Nevertheless, the above script should still be used, as it also adjusts the already spawned trading inventories in game saves (should this change come in a patch to an already released modification).

Chests and Containers

While the contents of chests and containers is typically set in the Spacer, it can be manipulated from the script. Much like the above script of NPC inventories, the script below iterates over all containers in the current world and removes a fraction of items of a given instance.

/*
 * Remove instances from containers (chests)
 */
func void reduceItemsInContainers(var int itemInst, var int minimumAmount, var float keepFraction) {
    // Gothic 1
    const int zCWorld__SearchVobListByBaseClass_G1 =  6250016; //0x5F5E20
    const int oCMobContainer__classDef_G1          =  9285504; //0x8DAF80
    const int oCMobContainer__Remove_G1            =  6831792; //0x683EB0

    // Gothic 2
    const int zCWorld__SearchVobListByBaseClass_G2 =  6439712; //0x624320
    const int oCMobContainer__classDef_G2          = 11212976; //0xAB18B0
    const int oCMobContainer__Remove_G2            =  7495664; //0x725FF0

    // Item instance name
    var string itemStr; itemStr = MEM_ReadString(MEM_GetSymbolByIndex(itemInst));

    // Create array that will contain all containers in the current world
    var int vobListPtr;  vobListPtr = MEM_ArrayCreate();
    var zCArray vobList; vobList    = _^(vobListPtr);

    // Search containers and fill the array
    var int vobTreePtr; vobTreePtr = _@(MEM_Vobtree);
    var int worldPtr;   worldPtr   = _@(MEM_World);
    var int classDef;   classDef   = MEMINT_SwitchG1G2(oCMobContainer__classDef_G1, oCMobContainer__classDef_G2);
    const int call = 0;
    if (CALL_Begin(call)) {
        CALL_PtrParam(_@(vobTreePtr));
        CALL_PtrParam(_@(vobListPtr));
        CALL_PtrParam(_@(classDef));
        CALL__thiscall(_@(worldPtr), MEMINT_SwitchG1G2(zCWorld__SearchVobListByBaseClass_G1,
                                                       zCWorld__SearchVobListByBaseClass_G2));
        call = CALL_End();
    };

    // Print some info to the zSpy
    var int s; s = SB_New();
    SB("Number of chests in this world: ");
    SBi(vobList.numInArray);
    SB(", keep ");
    SBf(mulf(castToIntf(keepFraction), mkf(100)));
    SB("% of all contained ");
    SB(itemStr);
    SB(" instances.");
    MEM_Info(SB_ToString());
    SB_Clear();

    // Reset counters
    var int totalInstances; totalInstances = 0;
    var int totalRemoved;   totalRemoved   = 0;

    // Iterate over all containers
    repeat(i, vobList.numInArray); var int i;
        var int containerPtr; containerPtr = MEM_ReadIntArray(vobList.array, i);
        var oCMobContainer container; container = _^(containerPtr);
        var int containerListPtr; containerListPtr = container.containList_next;

        // Reset counters
        var int keep;       keep       = 0;
        var int removed;    removed    = 0;
        var int origAmount; origAmount = 0;

        // Iterate over contents of this container
        while(containerListPtr);
            var zCListSort containerList; containerList = _^(containerListPtr);
            containerListPtr = containerList.next;

            // Get next item
            var int itemPtr; itemPtr = containerList.data;
            var oCItem item; item = _^(itemPtr);

            // Compare item instance
            if (item.instanz == itemInst) {
                origAmount = item.amount;
                keep = roundf(mulf(mkf(item.amount), castToIntf(keepFraction)));
                
                // Keep at least minimumAmount instances
                if (keep < minimumAmount) {
                    keep = minimumAmount;
                };
                item.amount = keep;

                // If none are left, remove all
                if (item.amount < 1) {
                    const int call2 = 0;
                    if (CALL_Begin(call2)) {
                        CALL_PtrParam(_@(itemPtr));
                        CALL__thiscall(_@(containerPtr), MEMINT_SwitchG1G2(oCMobContainer__Remove_G1,
                                                                           oCMobContainer__Remove_G2));
                        call2 = CALL_End();
                    };

                    // Update the number of removed instances
                    removed = origAmount;
                } else {
                    removed = origAmount - keep;
                };
                break;
            };
        end;

        // Update counters
        if (origAmount) {
            totalInstances += origAmount;
            totalRemoved   += removed;
        };
    end;

    // Free container array
    MEM_ArrayFree(vobListPtr);

    // Output summary
    SB("Removed ");
    SBi(totalRemoved);
    SB(" out of ");
    SBi(totalInstances);
    SB(" ");
    SB(itemStr);
    SB(" instances, kept ");
    SBi(totalInstances-totalRemoved);
    SB(".");
    MEM_Info(SB_ToString());
    SB_Destroy();
};

As for the NPC inventories, this script should be called at the end of the initialization of each world - only once for each world:

func void Init_World1()
{
    // ...

    var int reduceOnce;
    if (!reduceOnce) {
        MEM_Info("Reduce the number of projectiles in this world (once).");

        // NPC inventories
        _projectileReduceToFraction = 0.2;
        DoForAll(reduceItemsInInventory);

        // Containers
        reduceItemsInContainers(ItRw_Arrow, 2, 0.2); // ItAmArrow for Gothic 1
        reduceItemsInContainers(ItRw_Bolt,  2, 0.2); // ItAmBolt for Gothic 1

        reduceOnce = TRUE;
    };
};

World

For removing the projectiles that are lying in the world, the script below does not allow much adjustment regarding the number of kept instances. It merely leaves one instance in proximity of a certain way point. This means that if five projectiles are placed near a way point "WP_XYZ" all but one of them will be removed.

/*
 * Remove instances from world (all but one per way point)
 */
func void reduceItemsInWorld(var int itemInst) {
    // Addresses
    const int zCWayNet__GetNearestWaypoint_G1 = 7354960; //0x703A50
    const int zCWayNet__GetNearestWaypoint_G2 = 8050272; //0x7AD660

    // Item instance name
    var string itemStr; itemStr = MEM_ReadString(MEM_GetSymbolByIndex(itemInst));

    // Get array of instance that are lying in the world
    var int itemArrayPtr; itemArrayPtr = MEM_SearchAllVobsByName(itemStr);
    var zCArray itemArray; itemArray = _^(itemArrayPtr);

    // Print some info to the zSpy
    var int s; s = SB_New();
    SB("Found ");
    SBi(itemArray.numInArray);
    SB(" ");
    SB(itemStr);
    SB(" instances.");
    MEM_Info(SB_ToString());
    SB_Clear();

    // Create array to remember number of instances near each way point
    var int wpArrayPtr; wpArrayPtr = MEM_ArrayCreate();
    var zCArray wpArray; wpArray = _^(wpArrayPtr);
    wpArray.numAlloc = itemArray.numInArray; // Worst case scenario
    wpArray.array = MEM_Alloc(wpArray.numAlloc * sizeof_zString);

    // Iterate over found instances
    repeat(i, itemArray.numInArray); var int i;
        var int vobPtr; vobPtr = MEM_ReadIntArray(itemArray.array, i);
        if (!vobPtr) {
            continue;
        };
        var zCVob vob; vob = _^(vobPtr);

        // Get position of instance
        var int pos[3];
        pos[0] = vob.trafoObjToWorld[3];
        pos[1] = vob.trafoObjToWorld[7];
        pos[2] = vob.trafoObjToWorld[11];
        
        // Retrieve nearest way point
        var int posPtr; posPtr = _@(pos);
        var int wayNetPtr; wayNetPtr = MEM_World.wayNet;
        const int call = 0;
        if (CALL_Begin(call)) {
            CALL_PutRetValTo(_@(wpPtr));
            CALL__fastcall(_@(wayNetPtr), _@(posPtr), MEMINT_SwitchG1G2(zCWayNet__GetNearestWaypoint_G1,
                                                                        zCWayNet__GetNearestWaypoint_G2));
            call = CALL_End();
        };

        // Get way point name
        var int wpPtr;
        var string nearestWp;
        if (wpPtr) {
            var zCWaypoint wp; wp = _^(wpPtr);
            nearestWp = wp.name;
        } else {
            nearestWp = "";
        };

        // Decide which items to delete
        var int matchedWp; matchedWp = FALSE;

        // Iterate over all previously collected way points
        repeat(j, wpArray.numInArray); var int j;
            // If there is already an instance near that way point, remove it
            if (Hlp_StrCmp(MEM_ReadStringArray(wpArray.array, j), nearestWp)) {
                matchedWp = TRUE;
                Wld_RemoveItem(vob);
                break;
            };
        end;

        // If this is the first instance near that way point, remember the way point
        if (!matchedWp) {
            MEM_WriteStringArray(wpArray.array, wpArray.numInArray, nearestWp);
            wpArray.numInArray += 1;

            // Output some info
            SB("  (");
            SBi(wpArray.numInArray);
            SB(") Remove all but one instance in proximity of ");
            SB(nearestWp);
            SB(".");
            MEM_Info(SB_ToString());
            SB_Clear();
        };
    end;

    // Output summary
    SB("Removed ");
    SBi(itemArray.numInArray-wpArray.numInArray);
    SB(" out of ");
    SBi(itemArray.numInArray);
    SB(" ");
    SB(itemStr);
    SB(" instances, kept ");
    SBi(wpArray.numInArray);
    SB(".");
    MEM_Info(SB_ToString());
    SB_Destroy();

    // Free item arrays
    MEM_ArrayFree(wpArrayPtr);
    MEM_ArrayFree(itemArrayPtr);
};

As for the NPC inventories and containers, this script should be called at the end of the initialization of each world - only once for each world:

func void Init_World1()
{
    // ...

    var int reduceOnce;
    if (!reduceOnce) {
        MEM_Info("Reduce the number of projectiles in this world (once).");

        // NPC inventories
        _projectileReduceToFraction = 0.2;
        DoForAll(reduceItemsInInventory);

        // Containers
        reduceItemsInContainers(ItRw_Arrow, 2, 0.2); // ItAmArrow for Gothic 1
        reduceItemsInContainers(ItRw_Bolt,  2, 0.2); // ItAmBolt for Gothic 1

        // Loose items in the world
        reduceItemsInWorld(ItRw_Arrow); // ItAmArrow for Gothic 1
        reduceItemsInWorld(ItRw_Bolt);  // ItAmBolt for Gothic 1

        reduceOnce = TRUE;
    };
};

Complete Script

You can download the complete script here.

Have it parsed before the Startup.d and add the code below at the end of each Init_*-function (except the Init_Global and the Init_Sub_*-functions!) in the Startup.d.

    // ...

    var int reduceOnce;
    if (!reduceOnce) {
        MEM_Info("Reduce the number of projectiles in this world (once).");

        // NPC inventories
        _projectileReduceToFraction = 0.2;
        DoForAll(reduceItemsInInventory);

        // Containers
        reduceItemsInContainers(ItRw_Arrow, 2, 0.2); // ItAmArrow for Gothic 1
        reduceItemsInContainers(ItRw_Bolt,  2, 0.2); // ItAmBolt for Gothic 1

        // Loose items in the world
        reduceItemsInWorld(ItRw_Arrow); // ItAmArrow for Gothic 1
        reduceItemsInWorld(ItRw_Bolt);  // ItAmBolt for Gothic 1

        reduceOnce = TRUE;
    };