Modding:WorldGen API: Difference between revisions

From Vintage Story Wiki
m
Updated navbox to new code navbox.
(Marked this version for translation)
m (Updated navbox to new code navbox.)
 
(4 intermediate revisions by 2 users not shown)
Line 1: Line 1:
{{GameVersion|1.15}}
{{GameVersion|1.19.4}}
<languages/><translate>
<languages/><translate>
== Intro == <!--T:1-->
== Intro == <!--T:1-->


<!--T:2-->
<!--T:2-->
We will be walking through how to plug in and add your own features to the world generation code of Vintage Story by looking at a demo mod called VSTreasureChest. The full source code for this project can be found [https://github.com/anegostudios/VSTreasureChest on Github]. We are going to walk through coding it from scratch.
We will be walking through how to plug in and add your own features to the world generation code of Vintage Story by looking at a demo mod called VSTreasureChest. The full source code for this project can be found [https://github.com/anegostudios/vsmodexamples/tree/master/code_mods/TreasureChest on Github]. We are going to walk through coding it from scratch.


== VSTreasureChest Mod== <!--T:3-->
== VSTreasureChest Mod== <!--T:3-->
Line 52: Line 53:
Next we are going to add the '''/treasure''' command. To do this we must register a delegate so that we can be notified when the user types our command. We do this with the '''ICoreServerAPI.RegisterCommand''' method.
Next we are going to add the '''/treasure''' command. To do this we must register a delegate so that we can be notified when the user types our command. We do this with the '''ICoreServerAPI.RegisterCommand''' method.


In '''StartServerSide''' add the following line:
In '''StartServerSide''' add the following:
<syntaxhighlight lang="c#">
<syntaxhighlight lang="c#">
this.api.RegisterCommand("treasure", "Place a treasure chest with random items", "", PlaceTreasureChestInFrontOfPlayer, Privilege.controlserver);
api.ChatCommands.Create("treasure").RequiresPlayer()
    .WithDescription("Place a treasure chest with random items")
    .RequiresPrivilege(Privilege.controlserver)
    .HandleWith(new OnCommandDelegate(PlaceTreasureChestInFrontOfPlayer));
</syntaxhighlight>
</syntaxhighlight>
This is registering a '''treasure''' command with the server with a brief description that is used to describe the command for when the user types '''/help'''. The other important argument is the '''PlaceTreasureChestInFrontOfPlayer''' argument which is a reference to a method we haven't written yet. So lets add the following method below '''StartServerSide'''.
This is registering a '''treasure''' command with the server with a brief description that is used to describe the command for when the user types '''/help'''. The other important argument is the '''PlaceTreasureChestInFrontOfPlayer''' argument which is a reference to a method we haven't written yet. So lets add the following method below '''StartServerSide'''.
<syntaxhighlight lang="c#">
<syntaxhighlight lang="c#">
private void PlaceTreasureChestInFrontOfPlayer(IServerPlayer player, int groupId, CmdArgs args)
private TextCommandResult PlaceTreasureChestInFrontOfPlayer(TextCommandCallingArgs args)
{
{
}
}
Line 67: Line 71:
The first thing we need to do is figure out how to tell the API that we want a chest and not grass or stone or some other block. Every Block has a numerical ID that gets assigned when the server starts but this ID may change. Luckily there is a property of the Block class that identifies it and does not change. This is the Code property. Block codes can be found in '''Vintagestory\assets\blocktypes''' in json files. The one for chest is '''Vintagestory\assets\blocktypes\wood\generic\chest.json'''. If you open that file you will see at the very top the code property is set to "chest". We also need to append the type of the shape that basically tells the system which way the chest is facing. So for simplicity we are going to pick south. So the resulting block code we will be using is "chest-south". Ok lets see some code.
The first thing we need to do is figure out how to tell the API that we want a chest and not grass or stone or some other block. Every Block has a numerical ID that gets assigned when the server starts but this ID may change. Luckily there is a property of the Block class that identifies it and does not change. This is the Code property. Block codes can be found in '''Vintagestory\assets\blocktypes''' in json files. The one for chest is '''Vintagestory\assets\blocktypes\wood\generic\chest.json'''. If you open that file you will see at the very top the code property is set to "chest". We also need to append the type of the shape that basically tells the system which way the chest is facing. So for simplicity we are going to pick south. So the resulting block code we will be using is "chest-south". Ok lets see some code.
<syntaxhighlight lang="c#">
<syntaxhighlight lang="c#">
private void PlaceTreasureChestInFrontOfPlayer(IServerPlayer player, int groupId, CmdArgs args)
private TextCommandResult PlaceTreasureChestInFrontOfPlayer(TextCommandCallingArgs args)
{
{
    // Old, don't work on latest version
    // ushort blockID = api.WorldManager.GetBlockId("chest-south");
    // Block chest = api.WorldManager.GetBlockType(blockID);
    // chest.TryPlaceBlockForWorldGen(api.World.BlockAccessor, player.Entity.Pos.HorizontalAheadCopy(2).AsBlockPos, BlockFacing.UP);
    // New, work on latest version
     Block chest = api.World.GetBlock(new AssetLocation("chest-south"));
     Block chest = api.World.GetBlock(new AssetLocation("chest-south"));
     chest.TryPlaceBlockForWorldGen(api.World.BlockAccessor,  
     chest.TryPlaceBlockForWorldGen(api.World.BlockAccessor,  
         player.Entity.Pos.HorizontalAheadCopy(2).AsBlockPos, BlockFacing.UP, null
         args.Caller.Player.Entity.Pos.HorizontalAheadCopy(2).AsBlockPos, BlockFacing.UP, null
     );
     );
    return TextCommandResult.Success();
}
}
</syntaxhighlight>
</syntaxhighlight>
Line 192: Line 191:
     {
     {
         string nextItem = shuffleBag.Next();
         string nextItem = shuffleBag.Next();
         Item item = api.World.GetItem(nextItem);
         Item item = api.World.GetItem(new AssetLocation(nextItem));
         if (itemStacks.ContainsKey(nextItem))
         if (itemStacks.ContainsKey(nextItem))
         {
         {
Line 212: Line 211:
     foreach (ItemStack itemStack in itemStacks)
     foreach (ItemStack itemStack in itemStacks)
     {
     {
         slotNumber = Math.Min(slotNumber, chest.Inventory.QuantitySlots - 1);
         slotNumber = Math.Min(slotNumber, chest.Inventory.Count - 1);
         IItemSlot slot = chest.Inventory.GetSlot(slotNumber);
         IItemSlot slot = chest.Inventory[slotNumber];
         slot.Itemstack = itemStack;
         slot.Itemstack = itemStack;
         slotNumber++;
         slotNumber++;
Line 221: Line 220:
This method does just that. It advances the '''IItemSlot''' number each time, ensuring not to place more '''ItemStacks''' than there are '''IItemSlots''', and sets the '''IItemSlot.ItemStack''' value to the current '''ItemStack''' in the loop. We are almost there! We have all the pieces. Now we just need to get a reference to our chest that we have already placed and pass it to this method along with the '''ItemStacks'''.
This method does just that. It advances the '''IItemSlot''' number each time, ensuring not to place more '''ItemStacks''' than there are '''IItemSlots''', and sets the '''IItemSlot.ItemStack''' value to the current '''ItemStack''' in the loop. We are almost there! We have all the pieces. Now we just need to get a reference to our chest that we have already placed and pass it to this method along with the '''ItemStacks'''.


Lets go back to our '''PlaceTreasureChest''' method and replace the last line with the following code snippet.
Lets go back to our '''PlaceTreasureChest''' method and replace the contents with the following code snippet.
<syntaxhighlight lang="c#">
<syntaxhighlight lang="c#">
BlockPos pos = player.Entity.Pos.HorizontalAheadCopy(2).AsBlockPos;
Block chest = api.World.GetBlock(new AssetLocation("chest-south"));
BlockPos pos = args.Caller.Player.Entity.Pos.HorizontalAheadCopy(2).AsBlockPos;
chest.TryPlaceBlockForWorldGen(api.World.BlockAccessor, pos, BlockFacing.UP, null);
chest.TryPlaceBlockForWorldGen(api.World.BlockAccessor, pos, BlockFacing.UP, null);
IBlockEntityContainer chestEntity = (IBlockEntityContainer)api.World.BlockAccessor.GetBlockEntity(pos);
IBlockEntityContainer chestEntity = (IBlockEntityContainer)api.World.BlockAccessor.GetBlockEntity(pos);
AddItemStacks(chestEntity, MakeItemStacks());
AddItemStacks(chestEntity, MakeItemStacks());
return TextCommandResult.Success();
</syntaxhighlight>
</syntaxhighlight>
The first line is just capturing the '''BlockPos''' position object so that we can use it in two places. The next is the same as before, just placing the chest in the world. Next we go back to the '''IBlockAccessor''' to get the block entity at that position. It's important to call '''GetBlockEntity''' here because a chest is an '''Entity'''. An '''Entity''' is something that has extra behavior attached to it as opposed to a normal block. This method returns an '''IBlockEntity''' which is a generic interface for all block entities. However we specifically need a block entity that has an Inventory. Since we know the block entity we just placed is a chest then it's safe to to cast the returned '''IBlockEntity''' to an '''IBlockEntityContainer''' which is a specialized version of '''IBlockEntity''' that provides access to an '''Inventory'''. Now that we have that we pass it along to our '''AddItemStacks''' method we created earlier and additionally pass in the list of '''ItemStacks''' that are created by our '''MakeItemStacks''' method that we also created earlier. Now if you run the code again and type '''/treasure''' you should have random items in there! Try it several times and you will see them change.
The second line is just capturing the '''BlockPos''' position object so that we can use it in two places. The next is the same as before, just placing the chest in the world. Next we go back to the '''IBlockAccessor''' to get the block entity at that position. It's important to call '''GetBlockEntity''' here because a chest is an '''Entity'''. An '''Entity''' is something that has extra behavior attached to it as opposed to a normal block. This method returns an '''IBlockEntity''' which is a generic interface for all block entities. However we specifically need a block entity that has an Inventory. Since we know the block entity we just placed is a chest then it's safe to to cast the returned '''IBlockEntity''' to an '''IBlockEntityContainer''' which is a specialized version of '''IBlockEntity''' that provides access to an '''Inventory'''. Now that we have that we pass it along to our '''AddItemStacks''' method we created earlier and additionally pass in the list of '''ItemStacks''' that are created by our '''MakeItemStacks''' method that we also created earlier. Now if you run the code again and type '''/treasure''' you should have random items in there! Try it several times and you will see them change.


That's really cool and all but not real fun for a game play experience. We want the player to find these chests and encourage them to explore the world! So lets plug in to world gen next!
That's really cool and all but not real fun for a game play experience. We want the player to find these chests and encourage them to explore the world! So lets plug in to world gen next!
Line 237: Line 238:
Add the following to '''StartServerSide'''
Add the following to '''StartServerSide'''
<syntaxhighlight lang="c#">
<syntaxhighlight lang="c#">
this.api.Event.ChunkColumnGeneration(OnChunkColumnGeneration, EnumWorldGenPass.Vegetation, "standard");
this.api.Event.ChunkColumnGeneration(OnChunkColumnGeneration, EnumWorldGenPass.PreDone, "standard");
</syntaxhighlight>
</syntaxhighlight>


The first argument is a delegate, which is a reference to a method. You will get a compiler error after adding that line because we have not yet created the '''OnChunkColumnGeneration''' method. We will create that next. The second argument is an Enum that indicates which world generation pass we need to hook into. Vintage story uses several passes to generate the world. Different features are available during different world gen passes. Since we will be placing chests next to trees, we need trees to be in the world so we choose to be notified during the '''EnumWorldGenPass.Vegetation''' which tells the engine that we need neighbor chunks, block layers, tall grass, bushes and trees to be available.
The first argument is a delegate, which is a reference to a method. You will get a compiler error after adding that line because we have not yet created the '''OnChunkColumnGeneration''' method. We will create that next. The second argument is an Enum that indicates which world generation pass we need to hook into. Vintage story uses several passes to generate the world. Different features are available during different world gen passes. Since we will be placing chests next to trees, we need trees to be in the world so we choose to be notified during the '''EnumWorldGenPass.PreDone''' which tells the engine that we need neighbor chunks, block layers, tall grass, bushes and trees to be available.
<syntaxhighlight lang="c#">
<syntaxhighlight lang="c#">
private void OnChunkColumnGeneration(IServerChunk[] chunks, int chunkX, int chunkZ)
private void OnChunkColumnGeneration(IChunkColumnGenerateRequest request)
{
{
}
}
Line 272: Line 273:
Let's do a quick refactoring before we move on. Remember our '''PlaceTreasureChestInFrontOfPlayer''' method? The code in that to place the chest is going to be reused by our world gen code but we won't have a player in that case and we will also need to use a different '''IBlockAccessor''' to place our block. So let's refactor that to use a new method we call '''PlaceTreasureChest'''. So replace '''PlaceTreasureChestInFrontOfPlayer''' with the following.
Let's do a quick refactoring before we move on. Remember our '''PlaceTreasureChestInFrontOfPlayer''' method? The code in that to place the chest is going to be reused by our world gen code but we won't have a player in that case and we will also need to use a different '''IBlockAccessor''' to place our block. So let's refactor that to use a new method we call '''PlaceTreasureChest'''. So replace '''PlaceTreasureChestInFrontOfPlayer''' with the following.
<syntaxhighlight lang="c#">
<syntaxhighlight lang="c#">
private void PlaceTreasureChestInFrontOfPlayer(IServerPlayer player, int groupId, CmdArgs args)
private TextCommandResult PlaceTreasureChestInFrontOfPlayer(TextCommandCallingArgs args)
{
{
     PlaceTreasureChest(worldBlockAccessor, player.Entity.Pos.HorizontalAheadCopy(2).AsBlockPos);
     PlaceTreasureChest(api.World.BlockAccessor, args.Caller.Entity.Pos.HorizontalAheadCopy(2).AsBlockPos);
    return TextCommandResult.Success();
}
}


private bool PlaceTreasureChest(IBlockAccessor blockAccessor, BlockPos pos)
private bool PlaceTreasureChest(IBlockAccessor blockAccessor, BlockPos pos)
{
{
     ushort blockID = api.WorldManager.GetBlockId("chest-south");
     var blockID = api.WorldManager.GetBlockId(new AssetLocation("chest-south"));
     Block chest = api.WorldManager.GetBlockType(blockID);
     var chest = api.World.BlockAccessor.GetBlock(blockID);
     chest.TryPlaceBlockForWorldGen(blockAccessor, pos, BlockFacing.UP);
 
    IBlockEntityContainer chestEntity = (IBlockEntityContainer)blockAccessor.GetBlockEntity(pos);
     if (chest.TryPlaceBlockForWorldGen(blockAccessor, pos, BlockFacing.UP, null))
    if (chestEntity != null)
     {
     {
         AddItemStacks(chestEntity, MakeItemStacks());
         var block = blockAccessor.GetBlock(pos);
        System.Diagnostics.Debug.WriteLine("Placed treasure chest at " + pos.ToString(), new object[] { });
        if (block.EntityClass != chest.EntityClass)
        return true;
        {
    }
            return false;
    else
        }
    {
 
         System.Diagnostics.Debug.WriteLine("FAILED TO PLACE TREASURE CHEST AT " + pos.ToString(), new object[] { });
        var blockEntity = blockAccessor.GetBlockEntity(pos);
        return false;
        if (blockEntity != null)
        {
            blockEntity.Initialize(api);
            if (blockEntity is IBlockEntityContainer chestEntity)
            {
                AddItemStacks(chestEntity, MakeItemStacks());
                Debug.WriteLine("Placed treasure chest at " + pos, new object[] { });
                return true;
            }
         }
     }
     }
    Debug.WriteLine("FAILED TO PLACE TREASURE CHEST AT " + pos, new object[] { });
    return false;
}
}
</syntaxhighlight>
</syntaxhighlight>
Line 316: Line 329:
Replace the empty '''OnChunkColumnGeneration''' with the following:
Replace the empty '''OnChunkColumnGeneration''' with the following:
<syntaxhighlight lang="c#">
<syntaxhighlight lang="c#">
private void OnChunkColumnGeneration(IServerChunk[] chunks, int chunkX, int chunkZ)
private void OnChunkColumnGeneration(IChunkColumnGenerateRequest request)
{
{
     int chestsPlacedCount = 0;
     var chestsPlacedCount = 0;
     for (int i = 0; i < chunks.Length; i++)
     for (var i = 0; i < request.Chunks.Length; i++)
     {
     {
         if (ShouldPlaceChest())
         if (ShouldPlaceChest())
         {
         {
             BlockPos blockPos = new BlockPos();
             var blockPos = new BlockPos(Dimensions.NormalWorld);
             for (int x = 0; x < chunkSize; x++)
             for (var x = 0; x < chunkSize; x++)
             {
             {
                 for (int z = 0; z < chunkSize; z++)
                 for (var z = 0; z < chunkSize; z++)
                 {
                 {
                     for (int y = 0; y < worldBlockAccessor.MapSizeY; y++)
                     for (var y = 0; y < worldBlockAccessor.MapSizeY; y++)
                     {
                     {
                         if (chestsPlacedCount < MAX_CHESTS_PER_CHUNK)
                         if (chestsPlacedCount < MAX_CHESTS_PER_CHUNK)
                         {
                         {
                             blockPos.X = chunkX * chunkSize + x;
                             blockPos.X = request.ChunkX * chunkSize + x;
                             blockPos.Y = y;
                             blockPos.Y = y;
                             blockPos.Z = chunkZ * chunkSize + z;
                             blockPos.Z = request.ChunkZ * chunkSize + z;


                             BlockPos chestLocation = TryGetChestLocation(blockPos);
                             var chestLocation = TryGetChestLocation(blockPos);
                             if (chestLocation != null)
                             if (chestLocation != null)
                             {
                             {
                                 bool chestWasPlaced = PlaceTreasureChest(chunkGenBlockAccessor, chestLocation);
                                 var chestWasPlaced = PlaceTreasureChest(chunkGenBlockAccessor, chestLocation);
                                 if (chestWasPlaced)
                                 if (chestWasPlaced)
                                 {
                                 {
Line 346: Line 359:
                             }
                             }
                         }
                         }
                         else//Max chests have been placed for this chunk
                         else //Max chests have been placed for this chunk
                         {
                         {
                             return;
                             return;
Line 377: Line 390:
private void LoadTreeTypes(ISet<string> treeTypes)
private void LoadTreeTypes(ISet<string> treeTypes)
{
{
     WorldProperty treeTypesFromFile = api.Assets.TryGet("worldproperties/block/wood.json").ToObject<WorldProperty>();
     var treeTypesFromFile = api.Assets.TryGet("worldproperties/block/wood.json").ToObject<StandardWorldProperty>();
     foreach (WorldPropertyVariant variant in treeTypesFromFile.Variants)
     foreach (var variant in treeTypesFromFile.Variants)
     {
     {
         treeTypes.Add("log-" + variant.Code + "-ud");
         treeTypes.Add($"log-grown-{variant.Code.Path}-ud");
     }
     }
}
}
Line 394: Line 407:
private bool IsTreeLog(Block block)
private bool IsTreeLog(Block block)
{
{
     return treeTypes.Contains(block.Code);
     return treeTypes.Contains(block.Code.Path);
}
}
</syntaxhighlight>
</syntaxhighlight>
Line 410: Line 423:
             {
             {
                 Block underBlock = chunkGenBlockAccessor.GetBlock(pos);
                 Block underBlock = chunkGenBlockAccessor.GetBlock(pos);
                 if (IsTreeLog(underBlock)) continue;
                 if (IsTreeLog(underBlock))  
 
                {
                    continue;
                }
               
                 foreach (BlockFacing facing in BlockFacing.HORIZONTALS)
                 foreach (BlockFacing facing in BlockFacing.HORIZONTALS)
                 {
                 {
Line 427: Line 443:
</syntaxhighlight>
</syntaxhighlight>
There are some improvements that could be made to this algorithm. I list them in the Exercises below. However, now you should be able to run the code and find treasure chests in your world!!
There are some improvements that could be made to this algorithm. I list them in the Exercises below. However, now you should be able to run the code and find treasure chests in your world!!
== Summary ==
== Summary ==
You should now have an idea of how to register commands and place blocks during world gen. There's plenty more to explore. If you want to take this code further please see the suggested exercises below.
You should now have an idea of how to register commands and place blocks during world gen. There's plenty more to explore. If you want to take this code further please see the suggested exercises below.
Line 442: Line 456:
</ul>
</ul>


 
{{Navbox/codemodding}}
{{Navbox/modding|Vintage Story}}
Confirmedusers
536

edits