Modding:Концепция WorldGen

From Vintage Story Wiki
Revision as of 05:55, 10 August 2025 by FuzzyBot (talk | contribs) (Updating to match new version of source page)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Эта страница проверялась в последний раз для версии Vintage Story 1.20.12.


Other languages:

В этой статье рассказывается, как обрабатывается генерация мира в истории Vintage, и в какой степени его можно модифицировать. Он также предназначен в качестве общего введения для моддинга в отношении генерации мира.

Терминология

Важная терминология, на которую стоит обратить внимание при чтении этой статьи:

  • Чанк: структура данных, которая содержит 32x32x32. Следует отметить, что фрагменты не охватывают высоту мира.
  • Столбец фрагментов: (набор фрагментов, охватывающих вертикальную высоту мира. Учитывая высоту мира 256, столбец фрагментов будет представлять 8 вертикально расположенных фрагментов)
  • Фрагмент карты: Структура данных, содержащая информацию в столбце фрагмента, например высоту дождя или скопление снега
  • Карта региона: Структура данных, которая хранит информацию в области столбцов фрагментов 16x16, например погоду и карты, необходимые для создания мира

Концепция

Генерация мира — это процесс создания необходимых данных о мире, когда это требуется игроку. Генерация мира выполняется на основе региона, фрагмента карты и столбца фрагмента, каждый из которых действует как значимое подразделение мира. Каждый регион, фрагмент карты и столбец фрагментов создаются только один раз и хранят важные данные об этой области. Генерация мира очень модульная. Почти все сгенерированные части мира, такие как залежи руды, существа или деревья, указываются с помощью модов содержимого JSON. На уровне кодирования каждый этап генерации запускает событие, на которое базовая игра регистрирует обработчики. Обработчики событий можно зарегистрировать или удалить через API, чтобы изменить любую часть генерации.

Модификация контента

Используя моддинг контента, вот различные вещи, которые можно добавить, изменить или удалить из генерации мира. Свойства, относящиеся к генерации мира, обычно находятся по адресу Vintagestory/assets/survival/worldgen.

Модифицируемый контент

Вещь Описание
Депозиты Все, что «откладывается» в земле при генерации, например, руды или участки блоков, таких как гравий и мел.
Структуры Предопределенные структуры, такие как руины, найденные повсюду. На странице «Как использовать WorldEdit» есть инструкции по созданию и экспорту структур.
Деревья Размещение деревьев и кустарников
Слои блоков Слои блоков, обнаруженные над слоем породы
Блочные патчи Патчи блоков, а также растительность
Геологические провинции Геологические атрибуты и размещение
Рельеф План местности и общее обустройство местности

Usually the world generators are invoked to fill in a new chunk. Note that they can also be invoked by IWorldManagerAPI.PeekChunkColumn. This method basically runs the world generators on a test chunk. It generates a new chunk column from scratch, ignoring any loaded chunks or chunks on disk. The result is not saved in the loaded chunk dictionary or written to disk. BEStaticTranslocator uses this to generate chunks only up to the terrain pass (skips most of the passes) to test whether the location is suitable for the translocator exit.

World types

World generation event handlers are registered for a specific world type. The generator registration takes the world type as a string. These strings mirror the values in EnumWorldGenPreset. Typically one registers generators for the "standard" world type.

  • "standard" - the common world type
  • "superflat" - a flat world often used as a test world in creative mode
  • "empty"

Passes

The chunk column generation event is additionally subdivided into passes. Passes are essentially layers in the generation of a chunk column - first laying down the rock layer in one pass, and doing more general cross-chunk actions in later passes, such as flooding them with light or growing patches of grass. Passes exist under EnumWorldGenPass. The pass has to be specified when registering event handlers for chunk column generation.

Индекс Имя Описание Требуются соседние фрагменты
0 Нет Пока ничего не создано false
1 Местность Базовая местность, пласты горных пород, пещеры, слои блоков false
2 Особенности рельефа Месторождения, сооружения, пруды true
3 Растительность Глыбовые участки, кустарники и деревья, ручейки, залитые солнечным светом куски true
4 СоседСолнцеСветНаводнение Слой снега, солнечные лучи заливают близлежащие куски true
5 Готово Закончив генерацию, создайте существ, соседние фрагменты могут по-прежнему генерировать дополнительные вещи true
6 Готово Pass не запускается как событие -

События

Генерация региона ('IMapRegion), фрагмента карты (IMapChunk) или фрагментов, содержащихся в этом столбце фрагментов (IServerChunk и IWorldChunk '), запускает событие (в указанном порядке). Каждый из этих интерфейсов содержит определенные данные о данном регионе. Возможна регистрация обработчиков событий, при этом доступ предоставляется для данного интерфейса. Порядок, в котором назначаются обработчики, основан на порядке выполнения их конкретных систем модов.

The vanilla world generators happen to also be instances of ModStdWorldGen, which in turn inherits from ModSystem. Inheriting from these classes is not a requirement for custom world generators. However, the benefit of doing so for the vanilla world generators is that the execution order logic in ModSystem helps set the order that the world generators run in. More specifically, each ModSystem subclass overrides the ExecuteOrder method. The mod system sorts the mods based on their execution order, then calls their StartServerSide methods in that order. The vanilla world generators (which are each their own ModSystem) then register their event handlers from their StartServerSide methods. Effectively the order of event handler list ends up matching their mods' execution orders.

Inheriting from ModStdWorldGen also gives access to some helper methods for determining if any generated structures exist at a location. These are helpful for preventing overlaps with those structures.

GetWorldgenBlockAccessor

        /// <summary>
        /// If you require neighbour chunk data during world generation, you have to register to this event to receive access to the chunk generator thread. This method is only called once during server startup.
        /// </summary>
        /// <param name="handler"></param>
        void GetWorldgenBlockAccessor(WorldGenThreadDelegate handler);
</div>

    <div lang="en" dir="ltr" class="mw-content-ltr">
public delegate void WorldGenThreadDelegate(IChunkProviderThread chunkProvider);
</div>

    <div lang="en" dir="ltr" class="mw-content-ltr">
public interface IChunkProviderThread
    {
        /// <summary>
        /// Retrieve a customized interface to access blocks for generating chunks
        /// </summary>
        /// <param name="updateHeightmap">Whether or not SetBlock should update the heightmap</param>
        /// <returns></returns>
        IWorldGenBlockAccessor GetBlockAccessor(bool updateHeightmap);
    }

Registering for this event is practically required for a world generator, so that one obtains a block accessor that can modify the map regions, map chunks, or chunk columns in the other event handlers. The default block accessor cannot access world objects until they are done generating. Additionally, the thread safety of calling the default block accessor from a world generator (which runs on a world generation thread) is questionable. Although the vanilla code ends up occasionally calling the default block accessor from the world generation threads.

The event handler receives a IChunkProviderThread instance. That interface only has one method on it, GetBlockAccessor. That method returns a special block accessor for the generating chunks. Arguably all world generation code should set updateHeightmap to true so that the returned block accessor keeps the map chunk's RainHeightMap and WorldGenTerrainHeightMap accurate. For each column of blocks, the RainHeightMap is supposed to point to the top block that rain cannot fall through. It is basically a surface height map for the map chunk.

When directly querying this accessor for objects that are world generated (map regions, map chunks, and chunks), the accessor will return the copy being generated, or null if the object is not being generated. Note that in the TerrainFeatures and later passes, the neighboring chunks are available.

Accessing entities and blocks is more lenient: it will first try to access the block in the generating chunks and fall back to the already loaded chunks. Trying to access a block from an unloaded chunk that is not generating will log a warning then return null (or an air block depending on the method). When querying objects that are not created during world generation, this accessor will forward to the default system accessor. For example, trying to look up a block type by id (vs trying to look up a block at a position) will forward to the default system accessor.

There is one gotcha with this special block accessor. For performance reasons, the generator internally caches the last chunk and map chunk it accessed, so that future accesses to that chunk are quick. However, if the map generator is somehow called for the same chunk location twice in a row (generate the chunk, delete the chunk, then regenerate the chunk), the block accessor's cache can still be pointing at the old chunk instance. So any changes made through the accessor would go to the old chunk and ultimately get lost. Arguably this is a bug in the IWorldGenBlockAccessor.

So to workaround that odd behavior in IWorldGenBlockAccessor, IWorldGenBlockAccessor.BeginColumn should be called to clear the cache. This only needs to be done once every time your world generation handler is called. It does not need to be called more than once inside the generation handler, because the accessor is smart enough to update the cache if the chunk location is different (such as when switching which chunk in the column is getting modified). Your handler may skip calling BeginColumn if it will not use the accessor during that handler call.

MapRegionGeneration

        /// <summary>
        /// Event that is triggered whenever a new 16x16 section of column of chunks is being generated. It is always called before the ChunkGenerator and before the MapChunkGeneration event
        /// </summary>
        /// <param name="handler"></param>
        /// <param name="forWorldType">For which world types to use this generator</param>
        void MapRegionGeneration(MapRegionGeneratorDelegate handler, string forWorldType);
</div>

    <div lang="en" dir="ltr" class="mw-content-ltr">
public delegate void MapRegionGeneratorDelegate(IMapRegion mapRegion, int regionX, int regionZ, ITreeAttribute chunkGenParams = null);

This event is called when a new map region is generated. The generator may either modify mapRegion directly, or modify it using the block accessor obtained in the GetWorldgenBlockAccessor event.

The chunkGenParams parameter is typically null, but if the map region was triggered through a call to IWorldManagerAPI.LoadLoadChunkColumnPriority, then the map region generator receives a reference to the chunk generation parameters specified in ChunkLoadOptions. The chunk generation parameters are basically a way for the LoadLoadChunkColumnPriority caller to pass arbitrary data to the world generators.

Note that generating a single chunk column requires generating its map chunk, which in turn requires its map region and all immediate neighboring map regions (8 of them) to exist or be created. So the map region world handler may be called to generate a neighboring map region and receive a chunkGenParams intended for a chunk in the neighboring map region.

MapChunkGeneration

        /// <summary>
        /// Event that is triggered whenever a new column of chunks is being generated. It is always called before the ChunkGenerator event
        /// </summary>
        /// <param name="handler"></param>
        /// <param name="forWorldType">For which world types to use this generator</param>
        void MapChunkGeneration(MapChunkGeneratorDelegate handler, string forWorldType);
</div>

    <div lang="en" dir="ltr" class="mw-content-ltr">
public delegate void MapChunkGeneratorDelegate(IMapChunk mapChunk, int chunkX, int chunkZ);

This API is used to register a map chunk generator. This is called before the corresponding chunk column is generated. The map region that contains the map chunk, and the neighboring map regions, are loaded before calling the map chunk generator.

ChunkColumnGeneration

        /// <summary>
        /// Vintagestory uses this method to generate the basic terrain (base terrain + rock strata + caves) in full columns. Only called once in pass EnumWorldGenPass.TerrainNoise. Register to this event if you need acces to a whole chunk column during inital generation.
        /// </summary>
        /// <param name="handler"></param>
        /// <param name="pass"></param>
        /// <param name="forWorldType">For which world types to use this generator</param>
        void ChunkColumnGeneration(ChunkColumnGenerationDelegate handler, EnumWorldGenPass pass, string forWorldType);
</div>

    <div lang="en" dir="ltr" class="mw-content-ltr">
public delegate void ChunkColumnGenerationDelegate(IChunkColumnGenerateRequest request);
</div>

    <div lang="en" dir="ltr" class="mw-content-ltr">
public interface IChunkColumnGenerateRequest
    {
        IServerChunk[] Chunks { get; }
        int ChunkX { get; }
        int ChunkZ { get; }
        ITreeAttribute ChunkGenParams { get; }
        ushort[][] NeighbourTerrainHeight { get; }
        bool RequiresChunkBorderSmoothing { get; }
    }

ChunkColumnGeneration registers a chunk column generator. This is the finest granularity world generation event (there are no chunk generation or block generation events). These generators are responsible for filling in the chunks and blocks within the chunk column.

The generator will only be called in the specified pass. The earlier #Passes section lists the possible values.

When this generator is called, the map chunk is can be read either through any of the entries in the Chunks array, or through the world gen block accessor. The corresponding map region is also available, along with the surrounding map regions.

These fields are in IChunkColumnGenerateRequest

Chunks
the chunks that are being generated in the column. Chunks[0] is the bottom-most chunk. These chunks can either be modified directly, or they can be modified through the worldgen block accessor (obtained during the GetWorldgenBlockAccessor event).
ChunkX and ChunkY
the coordinates of the chunk column being generated. To convert these to block coordinates, multiply them by the chunk size. The chunk size can be obtained from ModStdWorldGen.chunksize (for generators that inherit from ModStdWorldGen), or from GlobalConstants.ChunkSize.
ChunkGenParams
custom generation parameters. By default these parameters are null. However, the parameters can be set in calls to IWorldManagerAPI.LoadChunkColumnPriority through the ChunkLoadOptions.ChunkGenParams field. For example, when a static translocator block requests the exit location chunk to be generated, it sets the structureChanceModifier and structureMaxCount attributes to increase the chances that the structure world generator creates an exit translocator in the exit chunk.
NeighbourTerrainHeight
terrain height of the neighboring chunks. This is only set if RequiresChunkBorderSmoothing is true. The array is first indexed by the cardinal direction of the neighboring chunks (8 directions). The second index is the combined x-z coordinate of the block within the neighboring chunk. The array contains copies of the WorldGenTerrainHeightMap from the neighboring chunks.
RequiresChunkBorderSmoothing

true if this requests borders columns that were generated with a previous world generation version. Some world generators use this as a hint that it should blend the content with the neighboring chunk, instead of using its default algorithm. Without this blending, some generators would create a stark contrast between old and new chunks after upgrading a server.

This only detects changes in the world generation version. The mapping between game versions and world gen versions can be obtained by looking at the git history for the WorldGenVersion field in vsapi/Config/GameVersion.cs.

Доступ к событиям

class Mod : ModSystem
{
    public override double ExecuteOrder()
    {
        // Make sure all delegates have been assigned
        return 10;
    }

    public override void StartServerSide(ICoreServerAPI api)
    {
        // Get the handler for interacting with world generation.
        IWorldGenHandler handler = api.Event.GetRegisteredWorldGenHandlers("standard");

        // Example registering events using a delegate.
        api.Event.ChunkColumnGeneration(/* Handler here */, EnumWorldGenPass.None, "standard");
        api.Event.MapChunkGeneration(/* Handler here */, "standard");
        api.Event.MapRegionGeneration(/* Handler here */, "standard");

        // Retrieving registered handlers.
        List<MapChunkGeneratorDelegate> onMapChunkGen = handler.OnMapChunkGen;
        List<ChunkColumnGenerationDelegate>[] onChunkColGen = handler.OnChunkColumnGen;
        List<MapRegionGeneratorDelegate> onMapRegionGen = handler.OnMapRegionGen;

        foreach (var deleg in onMapChunkGen)
        {
            // Printing class name of delegate instance.
            // This can be useful for finding and removing any particular event from the list.
            api.Server.LogEvent(deleg.Target.ToString());
        }
    } 
}

Content modding

Using content modding, here are the various things that can be added, modified or removed from world generation. Properties relative to world generation are generally located at Vintagestory/assets/survival/worldgen

Moddable content

Item Description
Deposits Anything that is "deposited" in the ground on generation, such as ores or patches of blocks like gravel and chalk
Structures Pre-defined structures, such as the ruins found throughout. The "How to use WorldEdit" page has instructions on building and exporting structures
Trees Tree and shrub placement
Block layers Layers of blocks found above the rock layer
Block patches Patches of blocks, also vegetation
Geological provinces Geological attributes and placement
Landforms Land layout and the general arrangement of terrain