Modding:Asset Patching in Code

From Vintage Story Wiki
Revision as of 18:58, 26 October 2024 by Bluelightning32 (talk | contribs) (Clarify that client-only mods should patch on the client side)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

The JSON asset files are parsed into C# files. JSON_Patching involves patching the assets before they are parsed. Asset patching in code involves patching the assets after they are converted to C# objects. This is different from Monkey patching, which involves using new code to patch compiled code.

JSON patches are tricky to get right, and patching the same change into hundreds of objects is very tedious. Patching in code works well for those cases. By applying the code patch to all assets that meet certain criteria, the patch can be more robust to the player adding more mods. Those new mods can add more assets, and as long as they meet the patch's criteria, they will automatically get patched.

Most of the examples below perform the patching in the AssetsFinalize stage, before the game is fully loaded. To better understand that, it helps to understand a little about the game's start up order.

  1. The server searches the file system to find which mods to load and what their load order is.
  2. StartPre is called on the server side for every loaded mod, in their load order.
  3. The server builds the asset search path, which includes all loaded mods.
  4. Start is called on the server side for every loaded mod, in their load order.
  5. AssetsLoaded is called on the server side for every loaded mod, in their load order. Some ModSystems in the VSEssentials mod, notably ModRegistryObjectTypeLoader, parse the asset files and create C# objects.
  6. AssetsFinalize is called on the server side for every loaded mod, in their load order. This is where code patches run.
  7. StartServerSide is called on the server side for every loaded mod, in their load order.
  8. The parsed C# assets should have stopped changing by this point, so the server calls OnLoaded on all assets.
  9. The C# assets (except config assets) are sent over the network to the client.
  10. The client loads mods and calls their methods in the same order.

For universal mods, they should only patch assets on the server side. Otherwise, the patch will be applied twice on the client (once by the server before sending over the assets and again on the client after receiving the patched assets). For server-only mods and client-only mods, they should patch on their side, without any special check. All examples on this page are for universal mods, and thus have the server side check. The following example comes from the VSSurvival mod and shows the server side check:

        public override void AssetsFinalize(ICoreAPI api)
        {
            // Needs to be done before assets are ready because it rewrites Behavior and CollectibleBehavior
            if (api.Side == EnumAppSide.Server) // No need to add it twice on the client
            {
                addReinforcementBehavior();
            }
        }

A downside to patching in code is that they are difficult to find in other mods. Let's say some modder gets a bug report that their new block wasn't working as expected on some server with 50 other mods. Most modders are used to looking for patches JSON patch files, but not C# files. So it may take them a long time to find out that your mod used a code patch to add a block behavior that is incompatible with their mod.

Note the dumpjson mod does show the effect of code patches, and can be useful for debugging in the above situation. Code patches are applied on the server side before transferring the assets to the client, and dumpjson runs on the client side and prints the assets it received from the server.

The examples below check whether the entries in the collectibles, blocks, items, etc.. arrays are null. These checks are necessary because internally VS creates an oversized array with null entries, and only resizes the array if it ends up being too small.

Attributes

Collectible attributes (not to confuse with itemstack attributes) can be modified using examples below:

Adding new attribute

This example adds the shelvable attribute to all collectibles:

using Newtonsoft.Json.Linq;
using Vintagestory.API.Common;
using Vintagestory.API.Datastructures;

public class WikiExamples : ModSystem
{
    public override void AssetsFinalize(ICoreAPI api)
    {
        if (api.Side != EnumAppSide.Server) {
            return;
        }
        foreach (CollectibleObject obj in api.World.Collectibles)
        {
            // Make sure collectible or its code is not null
            if (obj == null || obj.Code == null)
            {
                continue;
            }

            // Make sure attributes are not null
            obj.Attributes ??= new JsonObject(new JObject());
            
            // Make collectible storable on shelf
            obj.Attributes.Token["shelvable"] = JToken.FromObject(true);
        }
    }
}

Modify existing attribute

In this example we retrieve integer list, modify it, then put it back:

using Newtonsoft.Json.Linq;
using System.Collections.Generic;
using Vintagestory.API.Common;

public class WikiExamples : ModSystem
{
    public override void AssetsFinalize(ICoreAPI api)
    {
        if (api.Side != EnumAppSide.Server) {
            return;
        }
        foreach (CollectibleObject obj in api.World.Collectibles)
        {
            // Make sure collectible or its code is not null
            if (obj == null || obj.Code == null)
            {
                continue;
            }

            // Make sure attribute exists
            if (obj.Attributes != null && obj.Attributes.KeyExists("somelist"))
            {
                // Retrieve list
                List<int> list = obj.Attributes["somelist"].AsObject<List<int>>();
                
                // Add new value
                list.Add(1);

                // Put it back
                obj.Attributes.Token["somelist"] = JToken.FromObject(list);
            }
        }
    }
}

Behaviors

Behaviors of collectibles and entities can be modified too.

Note that blocks have two arrays of behaviors: CollectibleBehaviors and BlockBehaviors. When the VSEssentials mod loads blocks, it adds block behaviors to both arrays, and adds collectible behaviors to only CollectibleBehaviors. So code patches that add new block behaviors should maintain this invariant and add them to both arrays. Items only have have CollectibleBehaviors.

Adding behavior to collectible

Example of adding behaviors to items and blocks:

using Vintagestory.API.Common;
using Vintagestory.API.Util;
using Vintagestory.GameContent;

public class WikiExamples : ModSystem
{
    public override void AssetsFinalize(ICoreAPI api)
    {
        if (api.Side != EnumAppSide.Server) {
            return;
        }
        foreach (Item item in api.World.Items)
        {
            // Make sure item or its code is not null
            if (item == null || item.Code == null)
            {
                continue;
            }

            // Create collectible behavior
            CollectibleBehaviorGroundStorable behavior = new CollectibleBehaviorGroundStorable(item);
            
            // Add behavior to collectible
            item.CollectibleBehaviors = item.CollectibleBehaviors.Append(behavior);
        }

        foreach (Block block in api.World.Blocks)
        {
            // Make sure block or its code is not null
            if (block == null || block.Code == null)
            {
                continue;
            }

            // Create block block behavior
            BlockBehaviorCanIgnite blockBehavior = new BlockBehaviorCanIgnite(block);

            // Add block behavior to block
            block.CollectibleBehaviors = block.CollectibleBehaviors.Append(blockBehavior);
            block.BlockBehaviors = block.BlockBehaviors.Append(blockBehavior);

        }
    }
}

Adding behavior with properties to collectible

Example of adding behavior with properties to collectible:

using Newtonsoft.Json.Linq;
using Vintagestory.API.Common;
using Vintagestory.API.Datastructures;
using Vintagestory.API.Util;
using Vintagestory.GameContent;

public class WikiExamples : ModSystem
{
    public override void AssetsFinalize(ICoreAPI api)
    {
        if (api.Side != EnumAppSide.Server) {
            return;
        }
        foreach (CollectibleObject obj in api.World.Collectibles)
        {
            // Make sure collectible or its code is not null
            if (obj == null || obj.Code == null)
            {
                continue;
            }

            // Create collectible behavior
            CollectibleBehaviorGroundStorable behavior = new CollectibleBehaviorGroundStorable(obj);

            // Create empty properties
            JsonObject properties = new JsonObject(new JObject());

            // Fill properties
            properties.Token["layout"] = JToken.FromObject("Quadrants");

            // Initialize behavior with new properties
            behavior.Initialize(properties);

            // Add behavior to collectible
            obj.CollectibleBehaviors = obj.CollectibleBehaviors.Append(behavior);
        }
    }
}

Adding behavior with properties to block entity

Block entity behaviors can be added to block entities using blocks. In order for a block to have block entity behaviors, it must have a block entity class. The block entity class is set automatically when behaviors are added via JSON. However, it must be set explicitly in code patches, because they run after the VSEssentials code that sets the default value.

Example of adding block entity behavior with properties to blocks:

using Newtonsoft.Json.Linq;
using Vintagestory.API.Common;
using Vintagestory.API.Datastructures;
using Vintagestory.API.Util;

public class WikiExamples : ModSystem
{
    public override void AssetsFinalize(ICoreAPI api)
    {
        if (api.Side != EnumAppSide.Server) {
            return;
        }
        foreach (Block block in api.World.Blocks)
        {
            // Make sure collectible or its code is not null
            if (block == null || block.Code == null)
            {
                continue;
            }

            // Create block entity behavior
            BlockEntityBehaviorType behavior = new BlockEntityBehaviorType()
            {
                Name = "RainCollector"
            };

            // Create empty properties
            behavior.properties = new JsonObject(new JObject());

            // Fill properties
            behavior.properties.Token["litersPerTick"] = JToken.FromObject(0.1f);

            // Add block entity behavior to block
            block.BlockEntityBehaviors = block.BlockEntityBehaviors.Append(behavior);

            if (block.EntityClass == null)
            {
                block.EntityClass = "Generic";
            }
        }
    }
}


Code Modding
Basics Code Mods Preparing For Code Mods Creating A Code Mod
Tutorials
Advanced Server-Client Considerations Setting up your Development Environment Advanced Blocks Advanced Items Block and Item Interactions Block Behavior Block Entity Particle Effects World Access Inventory Handling Commands GUIs Network API Monkey patching (Harmony)
Data Management VCDBS format Savegame Moddata ModConfig File Chunk Moddata Serialization Formats TreeAttribute
Worldgen WorldGen API NatFloat EvolvingNatFloat
Rendering Shaders and Renderers
Icon Sign.png

Wondering where some links have gone?
The modding navbox is going through some changes! Check out Navigation Box Updates for more info and help finding specific pages.

Modding
Modding Introduction Getting Started Theme Pack
Content Modding Content Mods Developing a Content Mod Basic Tutorials Intermediate Tutorials Advanced Tutorials Content Mod Concepts
Code Modding Code Mods Setting up your Development Environment
Property Overview ItemEntityEntity BehaviorsBlockBlock BehaviorsBlock ClassesBlock EntitiesBlock Entity BehaviorsCollectible BehaviorsWorld properties
Workflows & Infrastructure Modding Efficiency TipsMod-engine compatibilityMod ExtensibilityVS Engine
Additional Resources Community Resources Modding API Updates Programming Languages List of server commandsList of client commandsClient startup parametersServer startup parameters
Example ModsAPI DocsGitHub Repository