[h1]A First look at the Foundry Mod Kit[/h1] Today I wanted to talk about one of the ways that you’ll be able to create mods in Foundry. This talk will be mostly focused on how you can script new components and systems to enhance existing buildings with new functionality. More specifically, we’ll be looking at taking one of my favorites: The Logistic Container [img]https://clan.cloudflare.steamstatic.com/images//38913947/edfc49e992c82e1044879e7efea9fc1784469602.png[/img] And turning it into: The Planter Box [img]https://clan.cloudflare.steamstatic.com/images//38913947/e7c17e231cec6ad31faf92c77450dbce25d58353.png[/img] [h1]Modest Beginnings[/h1] To make this new planter building, I don’t want to start from scratch. So the first thing I’m going to do is open the Foundry Asset Library and create a clone of the Logistic Container I: [img]https://clan.cloudflare.steamstatic.com/images//38913947/92aebef3080c13f4eac31d39a586358fcd03bc6c.png[/img] This sets me up with a copy of all the files I need and after buying a cool set of plants from the asset store, I can quickly setup my new planter box prefab: [img]https://clan.cloudflare.steamstatic.com/images//38913947/eaaae9751ed0ef0f1b35fe810effd6c817a2b714.png[/img] It’s probably worth mentioning at this point that I’m a programmer by trade so if you’re looking at this planter box and thinking “this doesn’t look too good”, you would be correct. That’s ok though because I wanted to make sure I went through all the steps of creating a mod myself, similar to how awesome people like you will be doing it at home. Now on to the scripting! [h1]One Component at a Time[/h1] The first thing I want to do is create some components to store all the data I’m going to need. If you’re familiar with Unity/MonoBehaviours, these are quite similar: [expand type=details] [code] // A component that represents a planter // box which can have a plant growing in it public class PlanterBox : IModComponent { // The plant growing in this planter box [Save] public ulong plantId; } // A component that represents a plant that // grows in a planter box public class Plant : IModComponent { // How many sim ticks till the plant is fully grown public int totalGrowthTimeInTicks = 100; // What item template spawns this plant public string itemTemplateId; // How long has this plant been growing for [Save] public int currentGrowthInTicks; } [/code] [/expand] Once you've created the script for your components, we can go ahead and add them to our Planter Box prefab: [img]https://clan.cloudflare.steamstatic.com/images//38913947/f2ac985ce0c5a531e5ccb20c7e001cf014149e08.png[/img] Now on to the systems! [h1] Systematic Behaviour [/h1] Systems are how you can add new behaviour to Foundry. First let's start with our PlantSimSystem. The only thing this system does is slowly grow our plants over time: [expand type=details] [code] // A system which handles the growth of plants // // [AddSystemToGameSimulation] automatically starts // this system when a game is started [AddSystemToGameSimulation] public class PlantSimSystem : SystemManager.System { // [LockstepTickRate(60)] means that this system is updated every // 60 simulation ticks. It's an easy way to reduce the cost of your // update. [LockstepTickRate(60)] public override void pstLockstep_tickUpdate(ulong tickId, DesyncHasher hasher) { // Here we iterate over all the Plant components foreach(var kvp in Mods.Components.getAll()) { // First we get the plant component var plant = kvp.Value; // Then we check to see if the plant is fully grown, // and if so, then we skip the rest of the update. bool isPlantFullyGrown = plant.currentGrowthInTicks >= plant.totalGrowthTimeInTicks; if (isPlantFullyGrown) continue; // And if we finally get to this point, // then we increase how many ticks this // plant has been growing for. plant.currentGrowthInTicks++; } } } [/code] [/expand] Next let's handle the PlanterBoxSimSystem. For this there are two things we need to take care of. We need to gather a mapping of item templates to plants so that we know what plant to spawn when we place an item into the logistic container and then we have to do the fun bit which is actually spawning a plant once we've found a valid seed in store: [expand type=details] [code] // A system which handles the spawning of plants // when the appropriate item has been placed in // storage. [AddSystemToGameSimulation] public class PlanterBoxSimSystem : SystemManager.System { // onAddedToManager() is called once when the system is first created // and added to the SystemManager public override void onAddedToManager() { // On startup we create a map of ItemTemplates->Plant Prototypes createItemTemplateToPlantPrototypeMap(); } // create the ItemTemplate->Plant Prototype Map void createItemTemplateToPlantPrototypeMap() { // Iterate all plant prototypes. // // A prototype is similar to a Unity GameObject. // It has a list of components and you can use it // to spawn entities in the simulation. foreach (var plantPrototype in Mods.Prototypes.getAllPrototypesWithComponent().Values) { // get the Plant component var plant = plantPrototype.getComponent(); // get the associated ItemTemplate var itemTemplate = AssetManager.getAsset(plant.itemTemplateId); // Map the item template to the specified plant prototype plantPrototypes[itemTemplate] = plantPrototype; } } // Update the simulation // // [LockstepTickRate(60)] tells the system manager to only update this system // every 60 ticks. [LockstepTickRate(60)] public override void pstLockstep_tickUpdate(ulong tickId, DesyncHasher hasher) { // Iterate all PlanterBox components foreach(var kvp in Mods.Components.getAll()) { // Get the planter box component var planterBox = kvp.Value; // If the planter box already has a valid plant, then // skip the rest of the update if (EntityManager.isValid(planterBox.plantId)) continue; // Get the planter box id var planterBoxId = kvp.Key; // Get a handle to the planter box inventory. // // Handles are used to interface with the native Foundry // entities that exist in the native C++ plugin. var planterBoxInventory = new StorageEntityHandle(planterBoxId).inventory; // Iterate all items in the planter box inventory foreach (var item in planterBoxInventory.items) { // If the item is not a plant, then skip it if (!plantPrototypes.TryGetValue(item.template, out var plantPrototype)) continue; // Remove the item from the inventory planterBoxInventory.tryRemoveItem(item.template.id); // Spawn a new plant from it's prototype var plantId = Mods.Entities.spawn(plantPrototype.id); // Have the planter box keep track of it's newly spawned plant planterBox.plantId = plantId; // Trigger an event that will be handled by the PlanterBoxRenderSystem // or any other mods who want to know when a plant has been planted. trigger(new OnEntityPlanted { plantId = plantId, planterBoxId = planterBoxId }); // If we've found a valid plant then we // can exit out of this loop break; } } } Dictionary plantPrototypes = new Dictionary(); } [/code] [/expand] Finally there's one last thing we're going to do which is to make a PlanterBoxRenderSystem which will spawn the plants once they get planted. For this we need to handle when the planter box is streamed in/out and when a new plant is planted. We're also going to scale up the plants as they grow. I know this has already been quite technical up till this point and though I can't promise you that this is going to be any less technical, I can promise you that this is the last piece of code. Promise. [expand type=details] [code] // A system which handles the rendering of plants when they're planted in a planter box // [AddSystemToGameClient] automatically adds the system to the game except when // the game is running in dedicated server mode. [AddSystemToGameClient] public class PlanterBoxRenderSystem : SystemManager.System { // Handle when the planter box GameObject is streamed in [EventHandler] public void handle(OnEntityStreamedIn evt) { // Keep track of the newly streamed in GameObject streamedInPlanterBoxes[evt.entityId] = evt.cmp; // Check to see if there's a plant we should be // visualizing trySpawnPlantVisualizer(evt.entityId); } // Handle when the planter box gets streamed out [EventHandler] public void handle(OnEntityStreamedOut evt) { // Remove the streamed out planter box. streamedInPlanterBoxes.Remove(evt.entityId); // Get the planter box component var planterBox = Mods.Components.get(evt.entityId); // Check to see if there's a plant GameObject that needs to be destroyed if(plantVisualizers.TryGetValue(planterBox.plantId, out var plantVisualizer)) { // If there is, then destroy it GameObject.Destroy(plantVisualizer.gameObject); // And remove the reference we had to it plantVisualizers.Remove(evt.entityId); } } // Handle the case where we've planted a new plant [EventHandler] public void handle(OnEntityPlanted evt) { // Spawn the plant visualizer if necessary trySpawnPlantVisualizer(evt.planterBoxId); } // This handles spawning the plant GameObject. // It will only be spawned if there's a valid plant // and if the planter box is streamed in. void trySpawnPlantVisualizer(ulong planterBoxId) { // If the planter box isn't streamed in, we can just skip the rest of this function if (!streamedInPlanterBoxes.TryGetValue(planterBoxId, out var planterBoxGO)) return; // Get the planter box component var planterBox = Mods.Components.get(planterBoxId); // If it does not have a plant then just skip the rest. if (!EntityManager.isValid(planterBox.plantId)) return; // Get the plant prefab to instantiate var plantPrefab = Mods.Entities.getEntityPrototype(planterBox.plantId).moddedGameObject; // Instantiate the prefab var plantGO = GameObject.Instantiate(plantPrefab); // Place the prefab at the plantLocator position of the planter box plantGO.transform.position = planterBoxGO.plantLocator.transform.position; // Keep track of our newly spawned plant. plantVisualizers.Add(planterBox.plantId, plantGO.gameObject); } // Handle the updating of the plant scales // // [RenderTickRate(1.0f)] means that this function will only be updated once // a second [RenderTickRate(1.0f)] public override void LateUpdate() { // Iterate all plant visualizers foreach(var kvp in plantVisualizers) { // Get the plant id var plantId = kvp.Key; // Get the plant component var plant = Mods.Components.get(plantId); // Calculate the new plant scale float plantScale = 0.1f + 0.9f * (float)plant.currentGrowthInTicks / (float)plant.totalGrowthTimeInTicks; // Adjust the plant's scale kvp.Value.transform.localScale = new Vector3(plantScale, plantScale, plantScale); } } // PlanterBox id to GameObject Dictionary streamedInPlanterBoxes = new Dictionary(); // Plant id to GameObject Dictionary plantVisualizers = new Dictionary(); } [/code] [/expand] And that's it! That's basically all the code necessary to have a plant, place it in a planter box and have it grow over time, all done inside of a mod and as a thank you for sticking around this far, here's a look at the PlanterBox mod working along side a Sprinkler mod that was made from one of the in game Pumps: [previewyoutube=RIAgVtB_cqk;full][/previewyoutube] [h1]Future Work[/h1] This is was just one of the ways that we're looking at making Foundry moddable. We're also working on Harmony support for those of you who want to hook into any C# functions we have. And for those of you who who are just looking to add/tweak/create crafting recipes/research/buildings, we're working on making it so you can do that by modifying data files, no programming necessary! That's all I have for this Foundry Friday. Once again... Thanks for listening!