diff --git a/code_mods/WorldGen_AlpineStory/.vscode/extensions.json b/code_mods/WorldGen_AlpineStory/.vscode/extensions.json
new file mode 100644
index 00000000..78988773
--- /dev/null
+++ b/code_mods/WorldGen_AlpineStory/.vscode/extensions.json
@@ -0,0 +1,6 @@
+{
+ "recommendations": [
+ "ms-dotnettools.csdevkit",
+ "ms-dotnettools.csharp"
+ ]
+}
\ No newline at end of file
diff --git a/code_mods/WorldGen_AlpineStory/.vscode/launch.json b/code_mods/WorldGen_AlpineStory/.vscode/launch.json
new file mode 100644
index 00000000..5d9b3bc2
--- /dev/null
+++ b/code_mods/WorldGen_AlpineStory/.vscode/launch.json
@@ -0,0 +1,61 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Launch Client (Debug)",
+ "type": "coreclr",
+ "request": "launch",
+ "program": "${env:VINTAGE_STORY}/Vintagestory.exe",
+ "linux": {
+ "program": "${env:VINTAGE_STORY}/Vintagestory"
+ },
+ "osx": {
+ "program": "${env:VINTAGE_STORY}/Vintagestory"
+ },
+ "preLaunchTask": "build",
+ "args": [
+ // "--playStyle" , "preset-surviveandbuild",
+ // "--openWorld" , "modding test world",
+ "--tracelog",
+ "--addModPath",
+ "${workspaceFolder}/AlpineStory/bin/Debug/Mods"
+ ],
+ "console": "internalConsole",
+ "stopAtEntry": false
+ },
+ {
+ "name": "Launch Server",
+ "type": "coreclr",
+ "request": "launch",
+ "program": "${env:VINTAGE_STORY}/VintagestoryServer.exe",
+ "linux": {
+ "program": "${env:VINTAGE_STORY}/VintagestoryServer"
+ },
+ "osx": {
+ "program": "${env:VINTAGE_STORY}/VintagestoryServer"
+ },
+ "preLaunchTask": "build",
+ "args": [
+ "--tracelog",
+ "--addModPath",
+ "${workspaceFolder}/AlpineStory/bin/Debug/Mods"
+ ],
+ "console": "internalConsole",
+ "stopAtEntry": false
+ },
+ {
+ "name": "CakeBuild",
+ "type": "coreclr",
+ "request": "launch",
+ "preLaunchTask": "build (Cake)",
+ "program": "${workspaceFolder}/CakeBuild/bin/Debug/net7.0/CakeBuild.dll",
+ "args": [],
+ "cwd": "${workspaceFolder}/CakeBuild",
+ "stopAtEntry": false,
+ "console": "internalConsole"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/code_mods/WorldGen_AlpineStory/.vscode/tasks.json b/code_mods/WorldGen_AlpineStory/.vscode/tasks.json
new file mode 100644
index 00000000..01267d21
--- /dev/null
+++ b/code_mods/WorldGen_AlpineStory/.vscode/tasks.json
@@ -0,0 +1,40 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "build",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "build",
+ "-c",
+ "Debug",
+ "${workspaceFolder}/AlpineStory/AlpineStory.csproj"
+ ],
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "package",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "run",
+ "--project",
+ "${workspaceFolder}/CakeBuild/CakeBuild.csproj"
+ ],
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "build (Cake)",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "build",
+ "-c",
+ "Debug",
+ "${workspaceFolder}/CakeBuild/CakeBuild.csproj"
+ ],
+ "problemMatcher": "$msCompile"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/code_mods/WorldGen_AlpineStory/CakeBuild/CakeBuild.csproj b/code_mods/WorldGen_AlpineStory/CakeBuild/CakeBuild.csproj
new file mode 100644
index 00000000..8e2c970c
--- /dev/null
+++ b/code_mods/WorldGen_AlpineStory/CakeBuild/CakeBuild.csproj
@@ -0,0 +1,19 @@
+
+
+ Exe
+ net7.0
+ $(MSBuildProjectDirectory)
+
+
+
+
+
+
+
+
+
+
+ $(VINTAGE_STORY)/VintagestoryAPI.dll
+
+
+
\ No newline at end of file
diff --git a/code_mods/WorldGen_AlpineStory/CakeBuild/Program.cs b/code_mods/WorldGen_AlpineStory/CakeBuild/Program.cs
new file mode 100644
index 00000000..762d6ae3
--- /dev/null
+++ b/code_mods/WorldGen_AlpineStory/CakeBuild/Program.cs
@@ -0,0 +1,110 @@
+using System;
+using System.IO;
+using Cake.Common;
+using Cake.Common.IO;
+using Cake.Common.Tools.DotNet;
+using Cake.Common.Tools.DotNet.Clean;
+using Cake.Common.Tools.DotNet.Publish;
+using Cake.Core;
+using Cake.Frosting;
+using Cake.Json;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using Vintagestory.API.Common;
+
+public static class Program
+{
+ public static int Main(string[] args)
+ {
+ return new CakeHost()
+ .UseContext()
+ .Run(args);
+ }
+}
+
+public class BuildContext : FrostingContext
+{
+ public const string ProjectName = "AlpineStory";
+ public string BuildConfiguration { get; set; }
+ public string Version { get; }
+ public string Name { get; }
+ public bool SkipJsonValidation { get; set; }
+
+ public BuildContext(ICakeContext context)
+ : base(context)
+ {
+ BuildConfiguration = context.Argument("configuration", "Release");
+ SkipJsonValidation = context.Argument("skipJsonValidation", false);
+ var modInfo = context.DeserializeJsonFromFile($"../{BuildContext.ProjectName}/modinfo.json");
+ Version = modInfo.Version;
+ Name = modInfo.ModID;
+ }
+}
+
+[TaskName("ValidateJson")]
+public sealed class ValidateJsonTask : FrostingTask
+{
+ public override void Run(BuildContext context)
+ {
+ if (context.SkipJsonValidation)
+ {
+ return;
+ }
+ var jsonFiles = context.GetFiles($"../{BuildContext.ProjectName}/assets/**/*.json");
+ foreach (var file in jsonFiles)
+ {
+ try
+ {
+ var json = File.ReadAllText(file.FullPath);
+ JToken.Parse(json);
+ }
+ catch (JsonException ex)
+ {
+ throw new Exception($"Validation failed for JSON file: {file.FullPath}{Environment.NewLine}{ex.Message}", ex);
+ }
+ }
+ }
+}
+
+[TaskName("Build")]
+[IsDependentOn(typeof(ValidateJsonTask))]
+public sealed class BuildTask : FrostingTask
+{
+ public override void Run(BuildContext context)
+ {
+ context.DotNetClean($"../{BuildContext.ProjectName}/{BuildContext.ProjectName}.csproj",
+ new DotNetCleanSettings
+ {
+ Configuration = context.BuildConfiguration
+ });
+
+
+ context.DotNetPublish($"../{BuildContext.ProjectName}/{BuildContext.ProjectName}.csproj",
+ new DotNetPublishSettings
+ {
+ Configuration = context.BuildConfiguration
+ });
+ }
+}
+
+[TaskName("Package")]
+[IsDependentOn(typeof(BuildTask))]
+public sealed class PackageTask : FrostingTask
+{
+ public override void Run(BuildContext context)
+ {
+ context.EnsureDirectoryExists("../Releases");
+ context.CleanDirectory("../Releases");
+ context.EnsureDirectoryExists($"../Releases/{context.Name}");
+ context.CopyFiles($"../{BuildContext.ProjectName}/bin/{context.BuildConfiguration}/Mods/mod/publish/*", $"../Releases/{context.Name}");
+ context.CopyDirectory($"../{BuildContext.ProjectName}/assets", $"../Releases/{context.Name}/assets");
+ context.CopyFile($"../{BuildContext.ProjectName}/modinfo.json", $"../Releases/{context.Name}/modinfo.json");
+ context.Zip($"../Releases/{context.Name}", $"../Releases/{context.Name}_{context.Version}.zip");
+ }
+}
+
+[TaskName("Default")]
+[IsDependentOn(typeof(PackageTask))]
+public class DefaultTask : FrostingTask
+{
+}
\ No newline at end of file
diff --git a/code_mods/WorldGen_AlpineStory/alpinestory/AlpineStoryModSystem.cs b/code_mods/WorldGen_AlpineStory/alpinestory/AlpineStoryModSystem.cs
new file mode 100644
index 00000000..3faebea0
--- /dev/null
+++ b/code_mods/WorldGen_AlpineStory/alpinestory/AlpineStoryModSystem.cs
@@ -0,0 +1,165 @@
+using System;
+using Vintagestory.API.Common;
+using Vintagestory.API.Server;
+using Vintagestory.Server;
+using Vintagestory.ServerMods;
+using SkiaSharp;
+using System.Collections.Generic;
+using Vintagestory.Common;
+
+/**
+
+ Notes on the world generation.
+
+ The world generation is made in several passes. The passes are defined in the enum EnumWorldGenPass.
+
+ Within a pass, the generation is done in layers are ordered based on the index given by the function ExecuteOrder().
+ The loop in StartServerSide deletes all currently registered world gen path (therefore with an index lower than this class ExecuteOrder).
+
+ The vanilla passes are the following:
+ - 0 GenTerra : creates the vanilla landscape heights.
+ - 0.1 GenRockStrata : layers the GenTerra height in different materials.
+ - 0.12 GenDungeons : generates the underground dungeons.
+ - 0.2 GenDeposits : generates the ore deposits.
+ - 0.3 GenStructures : generates the world structures.
+ - 0.4 GenBlockLayers : generates ice, grass...
+ - 0.5 GenPonds : generates lakes...
+ - 0.5 GenVegetationAndPatches : generates forests and plants.
+
+ The source code of the vanilla world generation can be found in the VS survival source code:
+ https://github.com/anegostudios/vssurvivalmod.git/Systems/WorldGen/Standard
+
+ This mode replaces the layers GenTerra and GenRockStrata to create a map based on a .png image.
+ The generation is purposously split in several steps for better code readability:
+ - 0 AlpineTerrain : Generates the granite main layer, and a layer of dirt on top of it (replaces GenTerra).
+ - 1 AlpineStrata : Adds a layer of another stone below the surface using the vanilla noise generator (replaces GenRockStrata).
+ - 2 AlpineFloor : parametrise the data that are used later to generate plants and the block cover.
+ - 3 AlpineFloorCorrection : Correcting some of the vanilla world gen features that don't match the expected result (dirt layer too thick, dirt or snow on cliff side).
+ - 4 AlpineRiver : Remove the plants from the rivers/lakes.
+
+*/
+namespace AlpineStoryMod
+{
+ public class AlpineStoryModSystem : ModStdWorldGen
+ {
+ ICoreServerAPI api;
+ SKBitmap height_map;
+ internal float data_width_per_pixel;
+ internal int min_height_custom = 90;
+ AlpineTerrain alpineTerrain;
+ AlpineStrata alpineStrata;
+ AlpineFloor alpineFloor;
+ AlpineFloorCorrection alpineFloorCorrection;
+ AlpineRiver alpineRiver;
+ BiomeGrid biomeGrid;
+ int[] regionMap;
+ int[] lakeMap;
+ UtilTool uTool;
+ public override bool ShouldLoad(EnumAppSide side)
+ {
+ return side == EnumAppSide.Server;
+ }
+
+ // The ExecuteOrder here is important as it has to be after all worldgen objects we want to delete (see StartServerSide function).
+ public override double ExecuteOrder()
+ {
+ return 0.2;
+ }
+
+ public override void StartServerSide(ICoreServerAPI api)
+ {
+ this.api = api;
+
+ // Removing all generator that was registered so far
+ foreach (int enumvalue in Enum.GetValues(typeof(EnumWorldGenPass)))
+ {
+ if (enumvalue < ((ServerMain)api.World).ModEventManager.WorldgenHandlers["standard"].OnChunkColumnGen.Length)
+ {
+ if (enumvalue != (int)EnumWorldGenPass.NeighbourSunLightFlood && enumvalue != (int)EnumWorldGenPass.PreDone)
+ {
+ var handlers = ((ServerMain)api.World).ModEventManager.WorldgenHandlers["standard"].OnChunkColumnGen[enumvalue] ;
+ List toRemove = new List();
+
+ if(handlers != null)
+ {
+ // Condition on which object type we want to remove
+ for (int i = 0; i < handlers.Count; i++){
+ var type = handlers[i].Method.DeclaringType;
+ if (type == typeof(GenTerra) ||
+ type == typeof(GenRockStrataNew) ||
+ type == typeof(GenTerraPostProcess)){
+ toRemove.Add(i);
+ }
+ }
+ for (int i = toRemove.Count - 1; i >= 0 ; i--){
+ handlers.RemoveAt(toRemove[i]) ;
+ }
+ }
+ }
+ }
+ }
+
+ // In this mod, the X - Z coordinates are scaled based on the map Y size
+ data_width_per_pixel = api.WorldManager.MapSizeY / 256;
+
+ // Change this boolean to True to generate a climate (temperature - rain) mapping, for debug/information purpose.
+ bool generateBiomeGrid = false ;
+
+ // We give a 2048 - 2048 offset of the map to not start on the borders
+ uTool = new UtilTool(api, 2048, 2048);
+
+ if(generateBiomeGrid){
+ biomeGrid = new BiomeGrid(api, height_map, data_width_per_pixel, min_height_custom);
+ api.Event.ChunkColumnGeneration(biomeGrid.OnChunkColumnGen, EnumWorldGenPass.Terrain, "standard");
+ }
+ else{
+ // Reading the height map that will be provided to all world generation passes
+
+ IAsset asset = this.api.Assets.Get(new AssetLocation("alpinestory:worldgen/processed.png"));
+ BitmapExternal bmpt = new BitmapExternal(asset.Data, asset.Data.Length, api.Logger);
+ height_map = bmpt.bmp;
+
+ if(height_map == null){
+ uTool.print("Current directory : "+System.IO.Directory.GetCurrentDirectory());
+ uTool.print("Files in current dir : "+System.IO.Directory.GetFiles(System.IO.Directory.GetCurrentDirectory()).ToString());
+ throw new Exception("Height map not found");
+ }
+
+ api.Logger.Notification("Image loaded : "+height_map.Width.ToString()+", "+height_map.Height.ToString());
+
+ chunksize = api.WorldManager.ChunkSize;
+
+ // The region maps correspond to macro maps of the world, (one pixel per chunk)
+ // - regionMap is used to set climates based on the local altitude
+ // - lakeMap is used to forbid forest in lakes
+ regionMap = uTool.build_region_map(height_map, api.WorldManager.ChunkSize, data_width_per_pixel, min_height_custom, api.WorldManager.MapSizeY, 0);
+ lakeMap = uTool.build_region_map(height_map, api.WorldManager.ChunkSize, data_width_per_pixel, min_height_custom, api.WorldManager.MapSizeY, 1);
+
+ // Int random generator used as criterion to spawn halite
+ Random rand = new Random();
+
+ // Creating an instance of each generation function
+ alpineTerrain = new AlpineTerrain(api, height_map, data_width_per_pixel, min_height_custom, uTool);
+ alpineStrata = new AlpineStrata(api, height_map, data_width_per_pixel, min_height_custom, uTool);
+ alpineFloor = new AlpineFloor(api, height_map, data_width_per_pixel, min_height_custom, regionMap, lakeMap, uTool);
+ alpineFloorCorrection = new AlpineFloorCorrection(api, height_map, data_width_per_pixel, min_height_custom, regionMap, rand, uTool);
+ alpineRiver = new AlpineRiver(api, height_map, data_width_per_pixel, min_height_custom, regionMap, uTool);
+
+ // Registering the generation function in the Terrain pass. It is not necessary to have them stored in different files.
+ api.Event.ChunkColumnGeneration(alpineTerrain.OnChunkColumnGen, EnumWorldGenPass.Terrain, "standard");
+ api.Event.ChunkColumnGeneration(alpineStrata.OnChunkColumnGen, EnumWorldGenPass.Terrain, "standard");
+ api.Event.ChunkColumnGeneration(alpineFloor.OnChunkColumnGen, EnumWorldGenPass.Terrain, "standard");
+ api.Event.ChunkColumnGeneration(alpineFloorCorrection.OnChunkColumnGen, EnumWorldGenPass.TerrainFeatures, "standard");
+ api.Event.ChunkColumnGeneration(alpineRiver.OnChunkColumnGen, EnumWorldGenPass.NeighbourSunLightFlood, "standard");
+ }
+
+ // Don't you dare removing that line, it would silently break some pass of the vanilla world gen.
+ api.Event.InitWorldGenerator(initWorldGen, "standard");
+ }
+
+ public void initWorldGen()
+ {
+ LoadGlobalConfig(api);
+ }
+ }
+}
\ No newline at end of file
diff --git a/code_mods/WorldGen_AlpineStory/alpinestory/Properties/launchSettings.json b/code_mods/WorldGen_AlpineStory/alpinestory/Properties/launchSettings.json
new file mode 100644
index 00000000..5038f3cc
--- /dev/null
+++ b/code_mods/WorldGen_AlpineStory/alpinestory/Properties/launchSettings.json
@@ -0,0 +1,16 @@
+{
+ "profiles": {
+ "Client": {
+ "commandName": "Executable",
+ "executablePath": "dotnet",
+ "commandLineArgs": "\"$(VINTAGE_STORY)/Vintagestory.dll\" --tracelog --addModPath \"$(ProjectDir)/bin/$(Configuration)/Mods\"",
+ "workingDirectory": "$(VINTAGE_STORY)"
+ },
+ "Server": {
+ "commandName": "Executable",
+ "executablePath": "dotnet",
+ "commandLineArgs": "\"$(VINTAGE_STORY)/VintagestoryServer.dll\" --tracelog --addModPath \"$(ProjectDir)/bin/$(Configuration)/Mods\"",
+ "workingDirectory": "$(VINTAGE_STORY)"
+ }
+ }
+}
\ No newline at end of file
diff --git a/code_mods/WorldGen_AlpineStory/alpinestory/alpinestory.csproj b/code_mods/WorldGen_AlpineStory/alpinestory/alpinestory.csproj
new file mode 100644
index 00000000..e6b5fdb9
--- /dev/null
+++ b/code_mods/WorldGen_AlpineStory/alpinestory/alpinestory.csproj
@@ -0,0 +1,57 @@
+
+
+ net7.0
+ false
+ bin\$(Configuration)\Mods\mod
+
+
+
+
+ $(VINTAGE_STORY)/VintagestoryAPI.dll
+ false
+
+
+ $(VINTAGE_STORY)\Vintagestory.exe
+ false
+
+
+ $(VINTAGE_STORY)\VintagestoryAPI.dll
+ false
+
+
+ $(VINTAGE_STORY)\VintagestoryLib.dll
+ false
+
+
+ $(VINTAGE_STORY)\Mods\VSCreativeMod.dll
+ false
+
+
+ $(VINTAGE_STORY)\Mods\VSSurvivalMod.dll
+ false
+
+
+ $(VINTAGE_STORY)\Mods\VSEssentials.dll
+ false
+
+
+
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+
+
+ $(VINTAGE_STORY)/Lib/Newtonsoft.Json.dll
+ False
+
+
+ $(VINTAGE_STORY)/Lib/SkiaSharp.dll
+ False
+
+
+
\ No newline at end of file
diff --git a/code_mods/WorldGen_AlpineStory/alpinestory/assets/alpinestory/worldgen/processed.png b/code_mods/WorldGen_AlpineStory/alpinestory/assets/alpinestory/worldgen/processed.png
new file mode 100644
index 00000000..6766f4fc
Binary files /dev/null and b/code_mods/WorldGen_AlpineStory/alpinestory/assets/alpinestory/worldgen/processed.png differ
diff --git a/code_mods/WorldGen_AlpineStory/alpinestory/modinfo.json b/code_mods/WorldGen_AlpineStory/alpinestory/modinfo.json
new file mode 100644
index 00000000..d5d4f128
--- /dev/null
+++ b/code_mods/WorldGen_AlpineStory/alpinestory/modinfo.json
@@ -0,0 +1,13 @@
+{
+ "type": "code",
+ "modid": "alpinestory",
+ "name": "Alpine Story",
+ "authors": [
+ "Zelfior"
+ ],
+ "description": "Immerse yourself in an alpine world.",
+ "version": "0.1.0",
+ "dependencies": {
+ "game": ""
+ }
+}
\ No newline at end of file
diff --git a/code_mods/WorldGen_AlpineStory/alpinestory/src/0_AlpineTerrain.cs b/code_mods/WorldGen_AlpineStory/alpinestory/src/0_AlpineTerrain.cs
new file mode 100644
index 00000000..814ecc78
--- /dev/null
+++ b/code_mods/WorldGen_AlpineStory/alpinestory/src/0_AlpineTerrain.cs
@@ -0,0 +1,163 @@
+using System;
+using Vintagestory.API.Common;
+using Vintagestory.API.Server;
+using Vintagestory.ServerMods;
+using Vintagestory.ServerMods.NoObf;
+using System.Collections;
+using System.Threading;
+using System.Threading.Tasks;
+using SkiaSharp;
+
+public class AlpineTerrain: ModStdWorldGen
+{
+ ICoreServerAPI api;
+ int maxThreads;
+ internal SKBitmap height_map;
+ internal float data_width_per_pixel;
+ internal int max_height_custom;
+ internal int min_height_custom;
+ internal UtilTool uTool;
+ public AlpineTerrain(){}
+ public AlpineTerrain(ICoreServerAPI api, SKBitmap height_map, float data_width_per_pixel, int min_height_custom, UtilTool uTool)
+ {
+ LoadGlobalConfig(api);
+
+ this.api = api;
+ this.height_map = height_map;
+
+ // The ColumnResult object will contain the data of the chunks to generate
+ columnResults = new ColumnResult[chunksize * chunksize];
+ for (int i = 0; i < chunksize * chunksize; i++) columnResults[i].ColumnBlockSolidities = new BitArray(api.WorldManager.MapSizeY);
+
+ // Initiating the number of threads to fasten the generation
+ maxThreads = Math.Min(Environment.ProcessorCount, api.Server.Config.HostedMode ? 4 : 10);
+
+ max_height_custom = api.WorldManager.MapSizeY;
+ this.data_width_per_pixel = data_width_per_pixel;
+ this.min_height_custom = min_height_custom;
+
+ // Tools dedicated to this mod mainly to interpolate between pixels and pre-process the heightmap
+ this.uTool = uTool;
+ }
+ public override double ExecuteOrder()
+ {
+ return 0.1;
+ }
+ ColumnResult[] columnResults;
+ public void OnChunkColumnGen(IChunkColumnGenerateRequest request)
+ {
+ generate(request.Chunks, request.ChunkX, request.ChunkZ, request.RequiresChunkBorderSmoothing);
+
+ }
+
+ private void generate(IServerChunk[] chunks, int chunkX, int chunkZ, bool requiresChunkBorderSmoothing)
+ {
+ int chunksize = this.chunksize;
+
+ int rockID = api.World.GetBlock(new AssetLocation("rock-granite")).Id ;
+
+ // // Store heightmap in the map chunk that can be used for ingame weather processing.
+ ushort[] rainheightmap = chunks[0].MapChunk.RainHeightMap;
+ ushort[] terrainheightmap = chunks[0].MapChunk.WorldGenTerrainHeightMap;
+
+ // Storing here the results for each X - Z coordinates (Y being the vertical) of the map pre-processing
+ int[] list_max_height = new int[chunksize*chunksize];
+
+ // Pre-processing the map : storing the height map per X-Z coordinate
+ for (int lZ = 0; lZ < chunksize; lZ++)
+ {
+ int worldZ = chunkZ * chunksize + lZ;
+ for (int lX = 0; lX < chunksize; lX++)
+ {
+ int worldX = chunkX * chunksize + lX;
+ int current_index = uTool.ChunkIndex2d(lX, lZ, chunksize);
+
+ int fakeWorldX = worldX + uTool.offsetX;
+ int fakeWorldZ = worldZ + uTool.offsetZ;
+
+ list_max_height[current_index] = (int) (min_height_custom + (max_height_custom - min_height_custom) * uTool.LerpPosHeight(fakeWorldX, fakeWorldZ, 0, data_width_per_pixel, height_map));
+
+ // Lowering the ground at rivers
+ if (uTool.LerpPosHeight(fakeWorldX, fakeWorldZ, 2, data_width_per_pixel, height_map) > 0.1){
+ list_max_height[current_index] -= 3;
+ }
+ }
+ }
+
+ // We find here all 2 high gap to increase the height there, it can prevent having 2 blocks wide steps, but is not necessary
+ int[] to_increase = uTool.analyse_chunk(list_max_height, chunkX, chunkZ, chunksize, min_height_custom, max_height_custom, data_width_per_pixel, height_map, 0);
+
+ for (int lZ = 0; lZ < chunksize*chunksize; lZ++){
+ if (to_increase[lZ] == 1){
+ list_max_height[lZ] += 1;
+ }
+ }
+
+ // For each X - Z coordinate of the chunk, storing the data in the column result. Multithreaded for faster process
+ Parallel.For(0, chunksize * chunksize, new ParallelOptions() { MaxDegreeOfParallelism = maxThreads }, chunkIndex2d => {
+
+ int current_thread = Thread.CurrentThread.ManagedThreadId;
+
+ int lX = chunkIndex2d % chunksize;
+ int lZ = chunkIndex2d / chunksize;
+ int worldX = chunkX * chunksize + lX;
+ int worldZ = chunkZ * chunksize + lZ;
+
+ BitArray columnBlockSolidities = columnResults[chunkIndex2d].ColumnBlockSolidities;
+
+ for (int posY = 1; posY < max_height_custom - 1; posY++)//80; posY++)
+ {
+ // The block solidity tells if the block will not be empty after the first pass.
+ columnBlockSolidities[posY] = posY < list_max_height[chunkIndex2d];
+ }
+ });
+
+ // Fills the chunk at height 0 of mantle blocks (indestructible block at the bottom of the map)
+ chunks[0].Data.SetBlockBulk(0, chunksize, chunksize, GlobalConfig.mantleBlockId);
+
+ /**
+ Setting the blocks data here.
+
+ The content of the chunks is stored in chunks[verticalChunkId].Data, which is an int array of size chunksize^3.
+
+ The Id to provide can be given by the following function, "rock-granite" being the name of a block for example.
+ api.World.GetBlock(new AssetLocation("rock-granite")).Id ;
+
+ */
+ for (int posY = 1; posY < max_height_custom - 1; posY++)
+ {
+ for (int lZ = 0; lZ < chunksize; lZ++)
+ {
+ int worldZ = chunkZ * chunksize + lZ;
+ for (int lX = 0; lX < chunksize; lX++)
+ {
+ int worldX = chunkX * chunksize + lX;
+
+ int mapIndex = uTool.ChunkIndex2d(lX, lZ, chunksize);
+
+ ColumnResult columnResult = columnResults[mapIndex];
+ bool isSolid = columnResult.ColumnBlockSolidities[posY];
+
+ if (isSolid)
+ {
+ // The rain maps help calculate where should it rain in the world
+ terrainheightmap[mapIndex] = (ushort)posY;
+ rainheightmap[mapIndex] = (ushort)posY;
+
+ // A function of the UtilTool class sets the block
+ // It is not as optimal as done in the vanilla worldgen, but more readable
+ uTool.setBlockId(lX, posY, lZ, chunksize, chunks, rockID);
+ }
+ }
+ }
+ }
+
+ ushort ymax = 0;
+ for (int i = 0; i < rainheightmap.Length; i++)
+ {
+ ymax = Math.Max(ymax, rainheightmap[i]);
+ }
+
+ chunks[0].MapChunk.YMax = ymax;
+ }
+}
\ No newline at end of file
diff --git a/code_mods/WorldGen_AlpineStory/alpinestory/src/1_AlpineStrata.cs b/code_mods/WorldGen_AlpineStory/alpinestory/src/1_AlpineStrata.cs
new file mode 100644
index 00000000..c63123fd
--- /dev/null
+++ b/code_mods/WorldGen_AlpineStory/alpinestory/src/1_AlpineStrata.cs
@@ -0,0 +1,144 @@
+using System;
+using Vintagestory.API.Common;
+using Vintagestory.API.Datastructures;
+using Vintagestory.API.Server;
+using Vintagestory.ServerMods;
+using System.Threading.Tasks;
+using SkiaSharp;
+
+public class AlpineStrata: ModStdWorldGen
+{
+ ICoreServerAPI api;
+ int maxThreads;
+ internal SKBitmap height_map;
+ internal float data_width_per_pixel;
+ internal int max_height_custom;
+ internal int min_height_custom;
+ internal UtilTool uTool;
+ internal MapLayerCustomPerlin[] strataNoises;
+ internal RockStrataConfig strata;
+ internal int[] rockIds;
+ public AlpineStrata(){}
+ public AlpineStrata(ICoreServerAPI api, SKBitmap height_map, float data_width_per_pixel, int min_height_custom, UtilTool uTool)
+ {
+ /**
+ This class will replace a layer of 10 blocks of another rock under the surface for a better gameplay experience.
+
+ The block choice is made based on a perlin noise map associated to each rock type:
+ When the noise is greater than a threshold, the associated rock will replace the in place granite.
+
+ */
+ LoadGlobalConfig(api);
+
+ this.api = api;
+ this.height_map = height_map;
+
+ maxThreads = Math.Min(Environment.ProcessorCount, api.Server.Config.HostedMode ? 4 : 10);
+
+ max_height_custom = api.WorldManager.MapSizeY;
+ this.data_width_per_pixel = data_width_per_pixel;
+ this.min_height_custom = min_height_custom;
+
+ this.uTool = uTool;
+
+ IAsset asset = api.Assets.Get("worldgen/rockstrata.json");
+ strata = asset.ToObject();
+
+ LoadGlobalConfig(api);
+
+ chunksize = api.WorldManager.ChunkSize;
+
+ // Storing the block id of different rock types.
+ rockIds = new int[14];
+ rockIds[0] = api.WorldManager.GetBlockId(new AssetLocation("rock-granite"));
+ rockIds[1] = api.WorldManager.GetBlockId(new AssetLocation("rock-basalt"));
+ rockIds[2] = api.WorldManager.GetBlockId(new AssetLocation("rock-andesite"));
+ rockIds[3] = api.WorldManager.GetBlockId(new AssetLocation("rock-peridotite"));
+ rockIds[4] = api.WorldManager.GetBlockId(new AssetLocation("rock-chalk"));
+ rockIds[5] = api.WorldManager.GetBlockId(new AssetLocation("rock-claystone"));
+ rockIds[6] = api.WorldManager.GetBlockId(new AssetLocation("rock-sandstone"));
+ rockIds[7] = api.WorldManager.GetBlockId(new AssetLocation("rock-shale"));
+ rockIds[8] = api.WorldManager.GetBlockId(new AssetLocation("rock-limestone"));
+ rockIds[9] = api.WorldManager.GetBlockId(new AssetLocation("rock-conglomerate"));
+ rockIds[10] = api.WorldManager.GetBlockId(new AssetLocation("rock-chert"));
+ rockIds[11] = api.WorldManager.GetBlockId(new AssetLocation("rock-phyllite"));
+ rockIds[12] = api.WorldManager.GetBlockId(new AssetLocation("rock-slate"));
+ rockIds[13] = api.WorldManager.GetBlockId(new AssetLocation("rock-bauxite"));
+
+ // Instanciating the perlin noise maps that will be used for the strata choice
+ strataNoises = new MapLayerCustomPerlin[strata.Variants.Length];
+ for (int i = 0; i < strata.Variants.Length; i++)
+ {
+ strataNoises[i] = new MapLayerCustomPerlin(api.World.Seed + 23423 + 500*i, new double[] { 14, 0, 0, 0 }, new double[] { 1 / 100.0 / 5, 1 / 50.0 / 5, 1 / 25.0 / 5, 1 / 12.5 / 5 }, new double[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13});
+ }
+ }
+ public override double ExecuteOrder()
+ {
+ return 0.13;
+ }
+ public void OnChunkColumnGen(IChunkColumnGenerateRequest request)
+ {
+ generate(request.Chunks, request.ChunkX, request.ChunkZ, request.RequiresChunkBorderSmoothing);
+ }
+ private void generate(IServerChunk[] chunks, int chunkX, int chunkZ, bool requiresChunkBorderSmoothing)
+ {
+ // The IntDataMap2D are 1D int containers that will store the noise map generated by the MapLayerCustomPerlin.
+ // It contains one value per X - Z coordinate.
+
+ IntDataMap2D[] intmaps = new IntDataMap2D[strata.Variants.Length];
+
+ for (int i = 0; i < strata.Variants.Length; i++)
+ {
+ intmaps[i] = new IntDataMap2D();
+ intmaps[i].Data = strataNoises[i].GenLayer(
+ chunkX*chunksize ,
+ chunkZ*chunksize ,
+ chunksize,
+ chunksize
+ );
+ }
+
+ /*
+ In this loop, we read the maximal height of each X - Z column from the chunks data.
+ */
+ int[] maxHeights = new int[chunksize*chunksize];
+ Parallel.For(0, chunksize*chunksize, new ParallelOptions() { MaxDegreeOfParallelism = maxThreads }, chunkIndex2d => {
+ int lX = chunkIndex2d % chunksize;
+ int lZ = chunkIndex2d / chunksize;
+
+ int mapIndex = uTool.ChunkIndex2d(lX, lZ, chunksize);
+ for(int lY = max_height_custom-1; lY > 1 ; lY--){
+ int chunkIndex = uTool.ChunkIndex3d(lX, lY%chunksize, lZ, chunksize);
+
+ if(chunks[lY/chunksize].Data[chunkIndex] == rockIds[0]){ // 0 means the block is empty
+ maxHeights[chunkIndex2d] = lY - 1;
+ break;
+ }
+ }
+ });
+
+ // Setting the value of the rock in the chunks data.
+ for (int lZ = 0; lZ < chunksize; lZ++)
+ {
+ for (int lX = 0; lX < chunksize; lX++)
+ {
+ int mapIndex = uTool.ChunkIndex2d(lX, lZ, chunksize);
+ int maxHeight=maxHeights[mapIndex];
+
+ for (int i = 1; i < strata.Variants.Length; i++)
+ {
+ if(intmaps[i].Data[mapIndex] > 8){
+ for (int j = 1; j < 10; j++)
+ {
+ if(maxHeight-j-5 > 0)
+ {
+ uTool.setBlockId(lX, maxHeight-j-5, lZ, chunksize, chunks, rockIds[Math.Min(13, i)]);
+ }
+ }
+ break;
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/code_mods/WorldGen_AlpineStory/alpinestory/src/2_AlpineFloor.cs b/code_mods/WorldGen_AlpineStory/alpinestory/src/2_AlpineFloor.cs
new file mode 100644
index 00000000..a4b7b74a
--- /dev/null
+++ b/code_mods/WorldGen_AlpineStory/alpinestory/src/2_AlpineFloor.cs
@@ -0,0 +1,194 @@
+using System;
+using Vintagestory.API.Datastructures;
+using Vintagestory.API.Server;
+using Vintagestory.ServerMods;
+using SkiaSharp;
+
+/*
+ Once the rocks landscape is generated, the vanilla world generation will replace the top layers of rock with dirt/sand/gravel,
+ and place animals based on regional maps.
+
+ The different heights maps are defined in this object:
+ chunks[0].MapChunk.MapRegion
+
+ We can notably find:
+ - ForestMap : Defines the forest density
+ - ClimateMap : Defines the climate (rain and temperature)
+ - ShrubMap : Defines the shurbs/foliages density
+
+ The region maps do not give the climate in the given chunk only but on the chunk in its surrounding.
+ The origin of the region maps is set on the chunk coordinates (chunkX, chunkZ) modulo 16 (16 chunks in a region).
+
+ Here is an example of what is expected in the climate map of the chunk (18, 25). the "o" are values to provide,
+ the "X" illustrates the position of the considered chunk. Borders are also to give to ensure the continuity of the
+ maps in the world.
+
+ 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
+ 14 o o | o o o o o o o o o o o o o o o o | o o
+ 15 o o | o o o o o o o o o o o o o o o o | o o
+ -------------------------------------------------------------------------------------
+ 16 o o | o o o o o o o o o o o o o o o o | o o
+ 17 o o | o o o o o o o o o o o o o o o o | o o
+ 18 o o | o o o o o o o o o o o o o o o o | o o
+ 19 o o | o o o o o o o o o o o o o o o o | o o
+ 20 o o | o o o o o o o o o o o o o o o o | o o
+ 21 o o | o o o o o o o o o o o o o o o o | o o
+ 22 o o | o o o o o o o o o o o o o o o o | o o
+ 23 o o | o o o o o o o o o o o o o o o o | o o
+ 24 o o | o o o o o o o o o o o o o o o o | o o
+ 25 o o | o o X o o o o o o o o o o o o o | o o
+ 26 o o | o o o o o o o o o o o o o o o o | o o
+ 27 o o | o o o o o o o o o o o o o o o o | o o
+ 28 o o | o o o o o o o o o o o o o o o o | o o
+ 29 o o | o o o o o o o o o o o o o o o o | o o
+ 30 o o | o o o o o o o o o o o o o o o o | o o
+ 31 o o | o o o o o o o o o o o o o o o o | o o
+ -------------------------------------------------------------------------------------
+ 32 o o | o o o o o o o o o o o o o o o o | o o
+ 33 o o | o o o o o o o o o o o o o o o o | o o
+
+ This map is stored in a 1D array using the same (X, Z) -> 1D mapping as used before.
+
+ However, it appears the origin of the map (low X and Z) is not at 0, 0 but at (length - 1, length - 1),
+ so the coordinates have to be flipped if the maps are set manually.
+
+*/
+public class AlpineFloor: ModStdWorldGen
+{
+ ICoreServerAPI api;
+ internal SKBitmap height_map;
+ internal float data_width_per_pixel;
+ internal int max_height_custom;
+ internal int bare_land_height_custom;
+ internal int min_height_custom;
+ internal UtilTool uTool;
+ int regionToChunkRatio;
+ internal int[] regionMap, lakeMap;
+ public AlpineFloor(){}
+ public AlpineFloor(ICoreServerAPI api, SKBitmap height_map, float data_width_per_pixel, int min_height_custom, int[] regionMap, int[] lakeMap, UtilTool uTool)
+ {
+ LoadGlobalConfig(api);
+
+ this.api = api;
+ this.height_map = height_map;
+ regionToChunkRatio = height_map.Width/chunksize;
+
+ max_height_custom = api.WorldManager.MapSizeY;
+
+ // We decide that above 90% of the world height, there shouldn't be any tree/bush anymore
+ bare_land_height_custom = (int) (max_height_custom*0.9);
+
+ this.data_width_per_pixel = data_width_per_pixel;
+ this.min_height_custom = min_height_custom;
+ this.regionMap = regionMap;
+ this.lakeMap = lakeMap;
+
+ this.uTool = uTool;
+ }
+ public void OnChunkColumnGen(IChunkColumnGenerateRequest request)
+ {
+ generate(request.Chunks, request.ChunkX, request.ChunkZ, request.RequiresChunkBorderSmoothing);
+ }
+
+ public override double ExecuteOrder()
+ {
+ return 0.15;
+ }
+ private void generate(IServerChunk[] chunks, int chunkX, int chunkZ, bool requiresChunkBorderSmoothing)
+ {
+ // Setting the global forestation to zero to better control the forest density
+ ITreeAttribute worldConfig = api.WorldManager.SaveGame.WorldConfiguration;
+ worldConfig.SetString("globalForestation", "0.");
+
+ // This value tells how many chunks are in a "region"
+ int globalRegionSize = api.WorldManager.RegionSize / chunksize;
+
+ // Offsetting the chunk by the same offset as defined in AlpineStoryModModSystem
+ int fakeChunkX = chunkX + uTool.offsetX/chunksize;
+ int fakeChunkZ = chunkZ + uTool.offsetZ/chunksize;
+
+ // Holds a forest density map, from 0 to 255
+ IntDataMap2D forestMap = chunks[0].MapChunk.MapRegion.ForestMap;
+
+ // build_mini_region_map builds a local region map as understood by the MapRegion tools.
+ int[] forest_height_map = uTool.build_mini_region_map(forestMap, fakeChunkX, fakeChunkZ, regionToChunkRatio, regionMap, globalRegionSize, 1);
+ int[] lake_height_map = uTool.build_mini_region_map(forestMap, fakeChunkX, fakeChunkZ, regionToChunkRatio, lakeMap, globalRegionSize, 1);
+
+ // If we are in a lake : no forest
+ // The forest takes the vanilla generated value, if the altitude is not too high
+ for(int i = 0; i < chunks[0].MapChunk.MapRegion.ForestMap.Data.Length; i++){
+ if (lake_height_map[i] == min_height_custom){
+ forestMap.Data[i] = Math.Clamp(forestMap.Data[i]-1, 0, getForestFromHeight(forest_height_map[i])) ;
+ }
+ else{
+ forestMap.Data[i] = 0;
+ }
+ }
+
+ // Holds temperature and rain fall.
+ // 16-23 bits = Red = temperature - 0 : frozen, 255 : all hail the cactus. (Height dependance strongly adds to this parameter)
+ // 8-15 bits = Green = rain
+ // 0-7 bits = Blue = unused
+ IntDataMap2D climateMap = chunks[0].MapChunk.MapRegion.ClimateMap;
+ climateMap.Data = uTool.build_mini_region_map(climateMap, fakeChunkX, fakeChunkZ, regionToChunkRatio, regionMap, globalRegionSize, 1);
+
+ for(int i = 0; i < climateMap.Data.Length; i++){
+ climateMap.Data[i] = (int)(0 + getRainFromHeight(climateMap.Data[i])*Math.Pow(2, 8) + getTemperatureFromHeight(climateMap.Data[i])*Math.Pow(2, 16)) ;
+ }
+
+ // Holds a beach density map
+ // No beach here, so all array set to 0
+ IntDataMap2D beachMap = chunks[0].MapChunk.MapRegion.BeachMap;
+ beachMap.Data = new int[beachMap.Size*beachMap.Size];
+
+ // Bushes density map, from 0 to 255
+ // The bushes density decreases with height
+ IntDataMap2D shrubMap = chunks[0].MapChunk.MapRegion.ShrubMap;
+ shrubMap.Data = uTool.build_mini_region_map(shrubMap, fakeChunkX, fakeChunkZ, regionToChunkRatio, regionMap, globalRegionSize, 0.5);
+
+ for(int i = 0; i < shrubMap.Data.Length; i++){
+ shrubMap.Data[i] = (int)getShrubFromHeight(shrubMap.Data[i]) ;
+ }
+ }
+ private float getRelativeHeight(int height){
+ return (float)(height - min_height_custom) / (float)(bare_land_height_custom - min_height_custom);
+ }
+ public int getRainFromHeight(int height){
+ /*
+ Returns 20000 at min_height_custom, linear increase with height
+ desert below 90
+ */
+ int min_value = 70;
+ int max_value = 200;
+ return (int)(min_value + (max_value - min_value) * getRelativeHeight(height));
+ }
+
+ public int getTemperatureFromHeight(int height){
+ /*
+ 150 at min height
+ 80 at max height
+ */
+ int min_value = 80;
+ int max_value = 150;
+ return (int)(150 - (max_value - min_value) * getRelativeHeight(height));
+ }
+ public int getShrubFromHeight(int height){
+ if (height > bare_land_height_custom) return 0;
+
+ return (int)(80 *(1 - getRelativeHeight(height)));
+ }
+ public int getForestFromHeight(int height){
+ /*
+ no forest above 60% of the bare_land_height_custom
+ linear increase below this height
+ */
+ float relative_height = getRelativeHeight(height);
+
+ if (relative_height > 0.6){
+ return 0;
+ }
+ else{
+ return (int)(relative_height/0.6*255);
+ }
+ }
+}
\ No newline at end of file
diff --git a/code_mods/WorldGen_AlpineStory/alpinestory/src/3_AlpineFloorCorrection.cs b/code_mods/WorldGen_AlpineStory/alpinestory/src/3_AlpineFloorCorrection.cs
new file mode 100644
index 00000000..6f8e6aac
--- /dev/null
+++ b/code_mods/WorldGen_AlpineStory/alpinestory/src/3_AlpineFloorCorrection.cs
@@ -0,0 +1,252 @@
+using System;
+using Vintagestory.API.Common;
+using Vintagestory.API.Server;
+using Vintagestory.ServerMods;
+using SkiaSharp;
+using System.Linq;
+
+public class AlpineFloorCorrection: ModStdWorldGen
+{
+ ICoreServerAPI api;
+ int maxThreads;
+ internal SKBitmap height_map;
+ internal float data_width_per_pixel;
+ internal int max_height_custom;
+ internal int bare_land_height_custom;
+ internal int min_height_custom;
+ internal UtilTool uTool;
+ internal int[] regionMap;
+ internal Random rand;
+ public AlpineFloorCorrection(){}
+ public AlpineFloorCorrection(ICoreServerAPI api, SKBitmap height_map, float data_width_per_pixel, int min_height_custom, int[] regionMap, Random rand, UtilTool uTool)
+ {
+ LoadGlobalConfig(api);
+
+ this.api = api;
+ this.height_map = height_map;
+
+ maxThreads = Math.Min(Environment.ProcessorCount, api.Server.Config.HostedMode ? 4 : 10);
+ max_height_custom = api.WorldManager.MapSizeY;
+ bare_land_height_custom = (int) (max_height_custom*0.9);
+
+ this.data_width_per_pixel = data_width_per_pixel;
+ this.min_height_custom = min_height_custom;
+ this.regionMap = regionMap;
+ this.rand = rand;
+
+ this.uTool = uTool;
+ }
+ public void OnChunkColumnGen(IChunkColumnGenerateRequest request)
+ {
+ generate(request.Chunks, request.ChunkX, request.ChunkZ, request.RequiresChunkBorderSmoothing);
+ }
+
+ public override double ExecuteOrder()
+ {
+ return 0.55;
+ }
+ private void generate(IServerChunk[] chunks, int chunkX, int chunkZ, bool requiresChunkBorderSmoothing)
+ {
+ int[] soil_array = new int[]{api.World.GetBlock(new AssetLocation("soil-medium-normal")).Id,
+ api.World.GetBlock(new AssetLocation("soil-medium-sparse")).Id ,
+ api.World.GetBlock(new AssetLocation("soil-medium-verysparse")).Id ,
+ api.World.GetBlock(new AssetLocation("soil-medium-none")).Id,
+ api.World.GetBlock(new AssetLocation("soil-low-normal")).Id,
+ api.World.GetBlock(new AssetLocation("soil-low-sparse")).Id,
+ api.World.GetBlock(new AssetLocation("soil-low-verysparse")).Id,
+ api.World.GetBlock(new AssetLocation("soil-low-none")).Id} ;
+
+ int rockID = api.World.GetBlock(new AssetLocation("rock-granite")).Id ;
+ int gravelID = api.World.GetBlock(new AssetLocation("gravel-granite")).Id ;
+ int muddyGravelID = api.World.GetBlock(new AssetLocation("muddygravel")).Id ;
+
+ int snowID = api.World.GetBlock(new AssetLocation("snowblock")).Id ;
+ int glacierID = api.World.GetBlock(new AssetLocation("glacierice")).Id ;
+ int waterID = api.World.GetBlock(new AssetLocation("water-still-7")).Id ;
+
+ int haliteID = api.World.GetBlock(new AssetLocation("rock-halite")).Id;
+
+ // Builds the chunk height map, and reduces the dirt layer thickness on the go to let the rocks appear on cliffs
+ int[] list_max_height = countHeightMap(chunks, rockID, snowID, glacierID, soil_array);
+
+ int[] grass_map = uTool.analyse_chunk(list_max_height, chunkX, chunkZ, chunksize, min_height_custom, max_height_custom, data_width_per_pixel, height_map, 1);
+ int[] remove_snow_map = uTool.analyse_chunk(list_max_height, chunkX, chunkZ, chunksize, min_height_custom, max_height_custom, data_width_per_pixel, height_map, 2);
+
+ // Replacing dirt by grass if the landscape is too steep
+ clearSteepGrass(chunks, grass_map, list_max_height, gravelID);
+
+ // Replacing now and ice by rock if the landscape is too steep
+ clearSteepSnow(chunks, remove_snow_map, gravelID);
+
+ // Removing glaciers that are not overlayed by snow
+ clearGlacier(chunks, rockID, glacierID, snowID);
+
+ // Make river beds
+ makeRiverBed(chunks, chunkX, chunkZ, waterID, muddyGravelID);
+
+ // Make lakes
+ uTool.makeLakes(chunks, chunkX, chunkZ, chunksize, waterID, muddyGravelID, min_height_custom, max_height_custom, data_width_per_pixel, height_map);
+
+ // Spawn Halite every 30 chunk
+ if (rand.Next(30) == 1){
+ spawnHalite(chunks, haliteID);
+ }
+ }
+ public void spawnHalite(IServerChunk[] chunks, int haliteID){
+ int posY;
+ int firstVoidFound;
+ int lX, lZ;
+ int haliteRadius = 4; // halite column radius
+
+ // Here, we loop on coluns to find a cave below the ground
+ // If we find one, we create a halite column starting at that height + 5 to the bottom of the map
+ for (int blockColumn = 0; blockColumn < chunksize*chunksize; blockColumn++){
+ lX = blockColumn%chunksize;
+ lZ = blockColumn/chunksize;
+ if (lX > haliteRadius && lZ > haliteRadius && lX < chunksize - haliteRadius && lZ < chunksize - haliteRadius){
+ posY = 1;
+
+ while(uTool.getBlockId(blockColumn%chunksize, posY, blockColumn/chunksize, chunksize, chunks) !=0) posY++;
+ firstVoidFound = posY;
+
+ while(posY < 0.9*max_height_custom && uTool.getBlockId(blockColumn%chunksize, posY, blockColumn/chunksize, chunksize, chunks) ==0) posY++;
+ if(posY < 0.9*max_height_custom){
+ for(int i=-haliteRadius; i 0.1){
+ altitude = (int) (min_height_custom + (max_height_custom - min_height_custom) * uTool.LerpPosHeight(worldX, worldZ, 0, data_width_per_pixel, height_map));
+
+ localRiverHeight = uTool.getRiverHeight(worldX, worldZ, min_height_custom, max_height_custom, data_width_per_pixel, height_map);
+
+ for(int posY = altitude-2; posY < localRiverHeight; posY++){
+ uTool.setBlockId(lZ%chunksize, posY, lZ/chunksize, chunksize, chunks, waterID, fluid:true);
+ }
+
+ if (altitude-2 == localRiverHeight){
+ uTool.setBlockId(lZ%chunksize, localRiverHeight, lZ/chunksize, chunksize, chunks, waterID, fluid:true);
+ }
+
+ uTool.setBlockId(lZ%chunksize, altitude-4, lZ/chunksize, chunksize, chunks, gravelID);
+ uTool.setBlockId(lZ%chunksize, altitude-3, lZ/chunksize, chunksize, chunks, gravelID);
+ }
+ }
+ }
+ public void clearGlacier(IServerChunk[] chunks, int rockID, int glacierID, int snowID){
+ int posY;
+
+ // Removes the glacier blocks that doesn't have snow block on it (looks more natural)
+ for (int lZ = 0; lZ < chunksize*chunksize; lZ++){
+ posY = max_height_custom - 1;
+
+ while(uTool.getBlockId(lZ%chunksize, posY, lZ/chunksize, chunksize, chunks) != snowID &&
+ posY > 0.6*max_height_custom) {
+ if(uTool.getBlockId(lZ%chunksize, posY, lZ/chunksize, chunksize, chunks) == glacierID){
+ uTool.setBlockId(lZ%chunksize, posY, lZ/chunksize, chunksize, chunks, rockID);
+ }
+ posY --;
+ }
+ }
+ }
+ public void clearSteepSnow(IServerChunk[] chunks, int[] remove_snow_map, int rockID){
+ int posY;
+
+ // Replacing snow by rock if the landscape is too steep
+ for (int lZ = 0; lZ < chunksize*chunksize; lZ++){
+ if (remove_snow_map[lZ] == 1){
+ posY = max_height_custom - 1;
+
+ while(uTool.getBlockId(lZ%chunksize, posY, lZ/chunksize, chunksize, chunks) == 0) posY --;
+
+ while(uTool.getBlockId(lZ%chunksize, posY, lZ/chunksize, chunksize, chunks) != rockID
+ && uTool.getBlockId(lZ%chunksize, posY, lZ/chunksize, chunksize, chunks) != 0 &&
+ posY > 2) {
+ uTool.setBlockId(lZ%chunksize, posY, lZ/chunksize, chunksize, chunks, rockID);
+ posY --;
+ }
+ }
+ }
+ }
+ public void clearSteepGrass(IServerChunk[] chunks, int[] grass_map, int[] list_max_height, int gravelID){
+ for (int lZ = 0; lZ < chunksize*chunksize; lZ++){
+ if (grass_map[lZ] == 1){
+ // Replacing dirt by gravel if the landscape is too steep
+ uTool.setBlockId(lZ%chunksize, list_max_height[lZ], lZ/chunksize, chunksize, chunks, gravelID);
+
+ if(list_max_height[lZ] < max_height_custom - 3){
+ if (uTool.getBlockId(lZ%chunksize, list_max_height[lZ] + 1, lZ/chunksize, chunksize, chunks) != 0
+ && uTool.getBlockId(lZ%chunksize, list_max_height[lZ] + 3, lZ/chunksize, chunksize, chunks) == 0 ){
+ uTool.setBlockId(lZ%chunksize, list_max_height[lZ] + 1, lZ/chunksize, chunksize, chunks, 0);
+ uTool.setBlockId(lZ%chunksize, list_max_height[lZ] + 2, lZ/chunksize, chunksize, chunks, 0);
+ }
+ }
+ }
+ }
+ }
+ public int[] countHeightMap(IServerChunk[] chunks, int rockID, int snowID, int glacierID, int[] soil_array){
+ int posY;
+ int currentBlock;
+ int mapIndex;
+ int[] list_max_height = new int[chunksize*chunksize];
+
+ for (int lZ = 0; lZ < chunksize; lZ++)
+ {
+ for (int lX = 0; lX < chunksize; lX++)
+ {
+ mapIndex = uTool.ChunkIndex2d(lX, lZ, chunksize);
+ posY = max_height_custom - 1;
+
+ while(uTool.getBlockId(lX, posY, lZ, chunksize, chunks) == 0) posY --;
+
+ currentBlock = uTool.getBlockId(lX, posY, lZ, chunksize, chunks);
+
+ while(currentBlock != rockID && !soil_array.Contains(currentBlock) && currentBlock != 0 && posY > 2) {
+ posY --;
+ currentBlock = uTool.getBlockId(lX, posY, lZ, chunksize, chunks);
+ if(currentBlock == snowID || currentBlock == glacierID){
+ list_max_height[mapIndex] = posY;
+ }
+ }
+
+ list_max_height[mapIndex] = Math.Max(posY, list_max_height[mapIndex]);
+
+ // Reducing the dirt layer thickness
+ if(soil_array.Contains(currentBlock)){
+ posY -- ;
+
+ currentBlock = uTool.getBlockId(lX, posY, lZ, chunksize, chunks);
+ while (currentBlock != rockID && currentBlock != 0 && posY > 2){
+ uTool.setBlockId(lX, posY, lZ, chunksize, chunks, rockID);
+ posY --;
+ currentBlock = uTool.getBlockId(lX, posY, lZ, chunksize, chunks);
+ }
+ }
+ }
+ }
+ return list_max_height;
+ }
+}
\ No newline at end of file
diff --git a/code_mods/WorldGen_AlpineStory/alpinestory/src/4_AlpineRiver.cs b/code_mods/WorldGen_AlpineStory/alpinestory/src/4_AlpineRiver.cs
new file mode 100644
index 00000000..bf775929
--- /dev/null
+++ b/code_mods/WorldGen_AlpineStory/alpinestory/src/4_AlpineRiver.cs
@@ -0,0 +1,95 @@
+using System;
+using Vintagestory.API.Common;
+using Vintagestory.API.Server;
+using Vintagestory.ServerMods;
+using SkiaSharp;
+
+public class AlpineRiver: ModStdWorldGen
+{
+ ICoreServerAPI api;
+ internal SKBitmap height_map;
+ internal float data_width_per_pixel;
+ internal int max_height_custom;
+ internal int min_height_custom;
+ internal UtilTool uTool;
+ internal int[] regionMap;
+ public AlpineRiver(){}
+ public AlpineRiver(ICoreServerAPI api, SKBitmap height_map, float data_width_per_pixel, int min_height_custom, int[] regionMap, UtilTool uTool)
+ {
+ LoadGlobalConfig(api);
+
+ this.api = api;
+ this.height_map = height_map;
+
+ this.min_height_custom = min_height_custom;
+ this.max_height_custom = api.WorldManager.MapSizeY;
+
+ this.data_width_per_pixel = data_width_per_pixel;
+ this.regionMap = regionMap;
+
+ this.uTool = uTool;
+ }
+ public void OnChunkColumnGen(IChunkColumnGenerateRequest request)
+ {
+ generate(request.Chunks, request.ChunkX, request.ChunkZ, request.RequiresChunkBorderSmoothing);
+ }
+
+ public override double ExecuteOrder()
+ {
+ return 0.05;
+ }
+ private void generate(IServerChunk[] chunks, int chunkX, int chunkZ, bool requiresChunkBorderSmoothing)
+ {
+ // We reiterate the river and lake making here, to remove the plants generated underwater by the vanilla worldgen.
+ int muddyGravelID = api.World.GetBlock(new AssetLocation("muddygravel")).Id ;
+ int waterID = api.World.GetBlock(new AssetLocation("water-still-7")).Id ;
+
+ // Clean river beds
+ cleanRiverBed(chunks, chunkX, chunkZ, waterID, muddyGravelID);
+
+ // Clean river beds
+ uTool.makeLakes(chunks, chunkX, chunkZ, chunksize, waterID, muddyGravelID, min_height_custom, max_height_custom, data_width_per_pixel, height_map);
+
+ }
+ public void cleanRiverBed(IServerChunk[] chunks, int chunkX, int chunkZ, int waterID, int gravelID){
+ float hasRiver;
+ int altitude;
+ int localRiverHeight;
+
+ for (int lZ = 0; lZ < chunksize*chunksize; lZ++){
+ int worldX = chunkX * chunksize + lZ%chunksize+ uTool.offsetX;
+ int worldZ = chunkZ * chunksize + lZ/chunksize + uTool.offsetZ;
+
+ hasRiver = uTool.LerpPosHeight(worldX, worldZ, 2, data_width_per_pixel, height_map);
+
+ if(hasRiver > 0.1){
+ altitude = (int) (min_height_custom + (max_height_custom - min_height_custom) * uTool.LerpPosHeight(worldX, worldZ, 0, data_width_per_pixel, height_map));
+
+ // Checking if we are not removing a tree
+ if(uTool.getBlockId(lZ%chunksize, altitude-3, lZ/chunksize, chunksize, chunks) == 0 ||
+ (uTool.getBlockId(lZ%chunksize, altitude-3, lZ/chunksize, chunksize, chunks) !=
+ uTool.getBlockId(lZ%chunksize, altitude+1, lZ/chunksize, chunksize, chunks))){
+
+ localRiverHeight = uTool.getRiverHeight(worldX, worldZ, min_height_custom, max_height_custom, data_width_per_pixel, height_map);
+
+ for(int posY = altitude-2; posY < localRiverHeight; posY++){
+ uTool.SetBlockAir(lZ%chunksize, posY, lZ/chunksize, chunksize, chunks);
+ uTool.setBlockId(lZ%chunksize, posY, lZ/chunksize, chunksize, chunks, waterID, fluid:true);
+ }
+
+ if (altitude-2 == localRiverHeight){
+ uTool.SetBlockAir(lZ%chunksize, localRiverHeight, lZ/chunksize, chunksize, chunks);
+ uTool.setBlockId(lZ%chunksize, localRiverHeight, lZ/chunksize, chunksize, chunks, waterID, fluid:true);
+ }
+
+ for(int posY = localRiverHeight; posY < localRiverHeight+3; posY++){
+ uTool.SetBlockAir(lZ%chunksize, posY, lZ/chunksize, chunksize, chunks);
+ }
+ }
+
+ uTool.setBlockId(lZ%chunksize, altitude-4, lZ/chunksize, chunksize, chunks, gravelID);
+ uTool.setBlockId(lZ%chunksize, altitude-3, lZ/chunksize, chunksize, chunks, gravelID);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/code_mods/WorldGen_AlpineStory/alpinestory/src/Bonus_BiomeGrid.cs b/code_mods/WorldGen_AlpineStory/alpinestory/src/Bonus_BiomeGrid.cs
new file mode 100644
index 00000000..c020c6dd
--- /dev/null
+++ b/code_mods/WorldGen_AlpineStory/alpinestory/src/Bonus_BiomeGrid.cs
@@ -0,0 +1,167 @@
+using System;
+using Vintagestory.API.Common;
+using Vintagestory.API.Datastructures;
+using Vintagestory.API.Server;
+using Vintagestory.ServerMods;
+using Vintagestory.ServerMods.NoObf;
+using System.Collections;
+using System.Threading;
+using System.Threading.Tasks;
+using SkiaSharp;
+
+public class BiomeGrid: ModStdWorldGen
+{
+ ICoreServerAPI api;
+ int maxThreads;
+ internal SKBitmap height_map;
+ internal float data_width_per_pixel;
+ internal int max_height_custom;
+ internal int bare_land_height_custom;
+ internal int min_height_custom;
+ internal UtilTool uTool;
+ ColumnResult[] columnResults;
+ public BiomeGrid(){}
+ public BiomeGrid(ICoreServerAPI api, SKBitmap height_map, float data_width_per_pixel, int min_height_custom)
+ {
+ LoadGlobalConfig(api);
+
+ this.api = api;
+ this.height_map = height_map;
+
+ // The ColumnResult object will contain the data of the chunks to generate
+ columnResults = new ColumnResult[chunksize * chunksize];
+ for (int i = 0; i < chunksize * chunksize; i++) columnResults[i].ColumnBlockSolidities = new BitArray(api.WorldManager.MapSizeY);
+
+
+ this.api = api;
+ this.height_map = height_map;
+
+ maxThreads = Math.Min(Environment.ProcessorCount, api.Server.Config.HostedMode ? 4 : 10);
+ max_height_custom = api.WorldManager.MapSizeY;
+ bare_land_height_custom = (int) (max_height_custom*0.9);
+
+ this.data_width_per_pixel = data_width_per_pixel;
+ this.min_height_custom = min_height_custom;
+
+ uTool = new UtilTool();
+
+ }
+ public void OnChunkColumnGen(IChunkColumnGenerateRequest request)
+ {
+ generate(request.Chunks, request.ChunkX, request.ChunkZ, request.RequiresChunkBorderSmoothing);
+ }
+
+ public override double ExecuteOrder()
+ {
+ return 1.5;
+ }
+ private void generate(IServerChunk[] chunks, int chunkX, int chunkZ, bool requiresChunkBorderSmoothing)
+ {
+ int chunksize = this.chunksize;
+
+ int rockID = api.World.GetBlock(new AssetLocation("rock-granite")).Id ;
+
+ // // Store heightmap in the map chunk that can be used for ingame weather processing.
+ ushort[] rainheightmap = chunks[0].MapChunk.RainHeightMap;
+ ushort[] terrainheightmap = chunks[0].MapChunk.WorldGenTerrainHeightMap;
+
+ // For each X - Z coordinate of the chunk, storing the data in the column result. Multithreaded for faster process
+ Parallel.For(0, chunksize * chunksize, new ParallelOptions() { MaxDegreeOfParallelism = maxThreads }, chunkIndex2d => {
+
+ int current_thread = Thread.CurrentThread.ManagedThreadId;
+
+ int lX = chunkIndex2d % chunksize;
+ int lZ = chunkIndex2d / chunksize;
+ int worldX = chunkX * chunksize + lX;
+ int worldZ = chunkZ * chunksize + lZ;
+
+ BitArray columnBlockSolidities = columnResults[chunkIndex2d].ColumnBlockSolidities;
+
+ for (int posY = 1; posY < max_height_custom - 1; posY++)
+ {
+ // The block solidity tells if the block will not be empty after the first pass.
+ columnBlockSolidities[posY] = posY < 150;
+ }
+ });
+
+ chunks[0].Data.SetBlockBulk(0, chunksize, chunksize, GlobalConfig.mantleBlockId);
+
+ /**
+ Setting the blocks data here.
+
+ The content of the chunks is stored in chunks[verticalChunkId].Data, which is an int array of size chunksize^3.
+
+ The Id to provide can be given by the following function, "rock-granite" being the name of a block for example.
+ api.World.GetBlock(new AssetLocation("rock-granite")).Id ;
+
+ */
+ for (int posY = 1; posY < max_height_custom - 1; posY++)
+ {
+ for (int lZ = 0; lZ < chunksize; lZ++)
+ {
+ int worldZ = chunkZ * chunksize + lZ;
+ for (int lX = 0; lX < chunksize; lX++)
+ {
+ int worldX = chunkX * chunksize + lX;
+
+ int mapIndex = uTool.ChunkIndex2d(lX, lZ, chunksize);
+
+ ColumnResult columnResult = columnResults[mapIndex];
+ bool isSolid = columnResult.ColumnBlockSolidities[posY];
+
+ if (isSolid)
+ {
+ terrainheightmap[mapIndex] = (ushort)posY;
+ rainheightmap[mapIndex] = (ushort)posY;
+
+ uTool.setBlockId(lX, posY, lZ, chunksize, chunks, rockID);
+ }
+ }
+ }
+ }
+
+ ushort ymax = 0;
+ for (int i = 0; i < rainheightmap.Length; i++)
+ {
+ ymax = Math.Max(ymax, rainheightmap[i]);
+ }
+
+ chunks[0].MapChunk.YMax = ymax;
+
+ // Holds a forest density map, from 0 to 255
+ IntDataMap2D forestMap = chunks[0].MapChunk.MapRegion.ForestMap;
+ for(int i = 0; i < forestMap.Data.Length; i++){
+ forestMap.Data[i] = 150;
+ }
+
+ // Holds temperature and rain fall.
+ // 16-23 bits = Red = temperature - 0 : frozen, 255 : all hail the cactus. (Height dependance adds to this parameter)
+ // 8-15 bits = Green = rain
+ // 0-7 bits = Blue = unused
+ IntDataMap2D climateMap = chunks[0].MapChunk.MapRegion.ClimateMap;
+
+ int grid_size = 16;
+ float factor = 255/(grid_size-1);
+
+ for(int i = 0; i < climateMap.Data.Length; i++){
+ int fakeChunkX = chunkX - chunkX%16 - climateMap.BottomRightPadding + i%climateMap.Size;
+ int fakeChunkZ = chunkZ - chunkZ%16 - climateMap.BottomRightPadding + i/climateMap.Size;
+
+ int rain = fakeChunkX%grid_size;
+ int temp = fakeChunkZ%grid_size;
+
+ climateMap.Data[i] = (int)(0 + Math.Min(255, (int)(factor*rain))*Math.Pow(2, 8) + Math.Min(255, (int)(factor*temp))*Math.Pow(2, 16)) ;
+ }
+
+ // // Holds a beach density map
+ IntDataMap2D beachMap = chunks[0].MapChunk.MapRegion.BeachMap;
+ beachMap.Data = new int[beachMap.Size*beachMap.Size];
+
+ // // Bushes density map, from 0 to 255
+ IntDataMap2D shrubMap = chunks[0].MapChunk.MapRegion.ShrubMap;
+
+ for(int i = 0; i < shrubMap.Data.Length; i++){
+ shrubMap.Data[i] = 255;
+ }
+ }
+}
\ No newline at end of file
diff --git a/code_mods/WorldGen_AlpineStory/alpinestory/src/utils.cs b/code_mods/WorldGen_AlpineStory/alpinestory/src/utils.cs
new file mode 100644
index 00000000..0f0cb9b1
--- /dev/null
+++ b/code_mods/WorldGen_AlpineStory/alpinestory/src/utils.cs
@@ -0,0 +1,304 @@
+using SkiaSharp;
+using System;
+using System.Collections;
+using System.Linq;
+using Vintagestory.API.Datastructures;
+using Vintagestory.API.Server;
+
+public class UtilTool
+{
+ internal ICoreServerAPI api;
+ internal int offsetX;
+ internal int offsetZ;
+ public UtilTool(){}
+ public UtilTool(ICoreServerAPI api, int offsetX, int offsetZ){
+ this.api = api;
+ this.offsetX = offsetX;
+ this.offsetZ = offsetZ;
+ }
+ public int ChunkIndex3d(int x, int y, int z, int chunksize)
+ {
+ /*
+ Vanilla function that gives the 1D index of a (x, y, z) coordinate in a chunk
+ */
+ return (y * chunksize + z) * chunksize + x;
+ }
+ public int ChunkIndex2d(int x, int z, int chunksize)
+ {
+ /*
+ Vanilla function that gives the 1D index of a (x, z) coordinate in a chunk column
+ */
+ return z * chunksize + x;
+ }
+ public void setBlockId(int lX, int lY, int lZ, int chunksize, IServerChunk[] chunks, int blockId, bool fluid=false){
+ /*
+ Sets the block at (lX, lY, lZ) coordinate in a chunk column to the given value.
+ If fluid is at true, the value is also assigned to the fluid data layer.
+ */
+ chunks[lY/chunksize].Data[ChunkIndex3d(lX, lY%chunksize, lZ, chunksize)] = blockId;
+
+ if(fluid){
+ chunks[lY/chunksize].Data[ChunkIndex3d(lX, lY%chunksize, lZ, chunksize)] = blockId;
+ chunks[lY/chunksize].Data.SetFluid(ChunkIndex3d(lX, lY%chunksize, lZ, chunksize), blockId);
+ }
+ }
+ public int getFluid(int lX, int lY, int lZ, int chunksize, IServerChunk[] chunks){
+ /*
+ Returns the fluid block ID at (lX, lY, lZ) coordinate in a chunk column.
+ */
+ return chunks[lY/chunksize].Data.GetFluid(ChunkIndex3d(lX, lY%chunksize, lZ, chunksize));
+ }
+ public int getBlockId(int lX, int lY, int lZ, int chunksize, IServerChunk[] chunks){
+ /*
+ Returns the block ID at (lX, lY, lZ) coordinate in a chunk column.
+ */
+ return chunks[lY/chunksize].Data[ChunkIndex3d(lX, lY%chunksize, lZ, chunksize)];
+ }
+ public void SetBlockAir(int lX, int lY, int lZ, int chunksize, IServerChunk[] chunks){
+ /*
+ Sets the block at (lX, lY, lZ) coordinate in a chunk column to an air block.
+ */
+ chunks[lY/chunksize].Data.SetBlockAir(ChunkIndex3d(lX, lY%chunksize, lZ, chunksize));
+ }
+ int mod(int x, int m) {
+ /*
+ Modulo function that necessarilly returns a value in [0, m[.
+ */
+ return (x%m + m)%m;
+ }
+ public int getOutBoundX(int x, int mapsize){
+ /*
+ Makes the symetry + modulo of the height map to fake an infinite world.
+ */
+ return (int)(Math.Abs(mapsize - 0.5 - mod(x, 2*mapsize))-0.5);
+ }
+ public float LerpPosHeight(int worldX, int worldZ, int color, float data_width_per_pixel, SKBitmap height_map){
+ /*
+ Lerp the Z value at the (worldX, worldZ) coordinates on the height map, on the given color chanel.
+ */
+ float current_x = (float)(worldX)/data_width_per_pixel ;
+ float current_z = (float)(worldZ)/data_width_per_pixel ;
+
+ float h0_0 = 0, h1_0 = 0, h0_1 = 0, h1_1 = 0;
+
+ // ICI TODO Cest a chier z0 a 4000, des trous a 2000 en worldX
+ int x0 = getOutBoundX((int)current_x, height_map.Width);
+ int z0 = getOutBoundX((int)current_z, height_map.Height);
+
+ int x1 = getOutBoundX((int)current_x+1, height_map.Width);
+ int z1 = getOutBoundX((int)current_z+1, height_map.Height);
+
+ if(color==0){
+ h0_0 = height_map.GetPixel(x0, z0).Red;
+ h1_0 = height_map.GetPixel(x1, z0).Red;
+
+ h0_1 = height_map.GetPixel(x0, z1).Red;
+ h1_1 = height_map.GetPixel(x1, z1).Red;
+ }
+ else if (color==1){
+ h0_0 = height_map.GetPixel(x0, z0).Green;
+ h1_0 = height_map.GetPixel(x1, z0).Green;
+
+ h0_1 = height_map.GetPixel(x0, z1).Green;
+ h1_1 = height_map.GetPixel(x1, z1).Green;
+
+ }
+ else if (color==2){
+ h0_0 = height_map.GetPixel(x0, z0).Blue;
+ h1_0 = height_map.GetPixel(x1, z0).Blue;
+
+ h0_1 = height_map.GetPixel(x0, z1).Blue;
+ h1_1 = height_map.GetPixel(x1, z1).Blue;
+ }
+
+ float x = current_x%1;
+ float z = current_z%1;
+
+ return ((1-x)*(1-z)*h0_0 + x*(1-z)*h1_0 + (1-x)*z*h0_1 + x*z*h1_1)/255;
+ }
+
+ public int[] analyse_chunk(int[] list_max_height, int chunkX, int chunkZ, int chunksize, int min_height_custom, int max_height_custom, float data_width_per_pixel, SKBitmap height_map, int criterion){
+ /*
+ Detects specific features on the height map, features description below.
+ */
+ int[] to_increase = new int[chunksize*chunksize];
+
+ for (int lZ = 0; lZ < chunksize; lZ++)
+ {
+ for(int lX = 0; lX < chunksize; lX++){
+ int[] neighbours = new int[4];
+
+ if ((lX - 1 >= 0) && (lZ - 1 >= 0)){
+ neighbours[0] = list_max_height[ChunkIndex2d(lX-1, lZ-1, chunksize)];
+ }
+
+ if ((lX - 1 >= 0) && (lZ + 1 < chunksize)){
+ neighbours[1] = list_max_height[ChunkIndex2d(lX-1, lZ+1, chunksize)];
+ }
+
+ if ((lX + 1 < chunksize) && (lZ + 1 < chunksize)){
+ neighbours[2] = list_max_height[ChunkIndex2d(lX+1, lZ+1, chunksize)];
+ }
+
+ if ((lX + 1 < chunksize) && (lZ - 1 >= 0)){
+ neighbours[3] = list_max_height[ChunkIndex2d(lX+1, lZ-1, chunksize)];
+ }
+
+ for(int i=0; i<4; i++){
+ if(neighbours[i] == 0) neighbours[i] = list_max_height[ChunkIndex2d(lX, lZ, chunksize)];
+ }
+
+ // Finds 2 blocks high steps
+ if (criterion == 0){
+ if(neighbours.Max() > list_max_height[ChunkIndex2d(lX, lZ, chunksize)] + 1){
+ to_increase[ChunkIndex2d(lX, lZ, chunksize)] = 1;
+ }
+ }
+ // Finds steep clifs
+ else if (criterion == 1){
+ if(neighbours.Max() - neighbours.Min() > 3){
+ to_increase[ChunkIndex2d(lX, lZ, chunksize)] = 1;
+ }
+ }
+ // Finds steep cliffs without its edges
+ else if (criterion == 2){
+ if(Math.Max(neighbours.Max() - list_max_height[ChunkIndex2d(lX, lZ, chunksize)], list_max_height[ChunkIndex2d(lX, lZ, chunksize)] - neighbours.Min()) > 2
+ && Math.Min(neighbours.Max() - list_max_height[ChunkIndex2d(lX, lZ, chunksize)], list_max_height[ChunkIndex2d(lX, lZ, chunksize)] - neighbours.Min()) > 1 ){
+ to_increase[ChunkIndex2d(lX, lZ, chunksize)] = 1;
+ }
+ }
+ }
+ }
+ return to_increase;
+ }
+
+ /**
+ Size height map: 4000 * 4000
+ Chunk size 32
+
+ region map 125 * 125, each value gives the height average of the chunk
+ */
+ public int[] build_region_map(SKBitmap height_map, int chunkSize, float data_width_per_pixel, int min_height_custom, int max_height_custom, int index, bool average=false){
+ int regionToChunkRatio = height_map.Width/chunkSize;
+ int[] regionMap = new int[regionToChunkRatio*regionToChunkRatio];
+
+ for(int chunkX=0; chunkX < regionToChunkRatio; chunkX++){
+ for(int chunkZ=0; chunkZ < regionToChunkRatio; chunkZ++){
+
+ int averageLocalHeight = 0;
+ int maximumLocalHeight = 0;
+
+ for(int i=0; i < chunkSize; i++){
+ for(int j=0; j < chunkSize; j++){
+ int localHeight = (int) (min_height_custom + (max_height_custom - min_height_custom) * LerpPosHeight(chunkX*chunkSize + i - chunkSize/2, chunkZ*chunkSize + j - chunkSize/2, index, data_width_per_pixel, height_map));
+
+ if(average){
+ averageLocalHeight += localHeight;
+ }
+ else {
+ maximumLocalHeight = Math.Max(maximumLocalHeight, localHeight);
+ }
+
+ }
+ }
+
+ int fakeChunkX = regionToChunkRatio-1-chunkX;
+ int fakeChunkZ = regionToChunkRatio-1-chunkZ;
+
+ // For some reason, the data maps are transposed compared to the world map, hence the regionToChunkRatio - 1 - XXX
+ if(average){
+ regionMap[ChunkIndex2d(fakeChunkX, fakeChunkZ, regionToChunkRatio)] = averageLocalHeight / (chunkSize*chunkSize);
+ }
+ else {
+ regionMap[ChunkIndex2d(fakeChunkX, fakeChunkZ, regionToChunkRatio)] = maximumLocalHeight;
+ }
+ }
+ }
+ return regionMap;
+ }
+
+ /**
+ region map: 4000 * 4000
+ Chunk size 32
+
+ region map 125 * 125, each value gives the height average of the chunk
+ */
+ public int[] build_mini_region_map(IntDataMap2D referenceMap, int chunkX, int chunkZ, int regionToChunkRatio, int[] regionMap, int globalRegionSize, double ratio){
+ /*
+ Builds the local region map at the given chunk coordinates, at the referenceMap dimensions.
+ */
+ int regionSize = referenceMap.Size;
+ int regionOffset = referenceMap.TopLeftPadding;
+
+ int[] miniRegionMap = new int[regionSize*regionSize];
+
+ for(int i = 0; i < miniRegionMap.Length; i++){
+ int fakeChunkX = (int)(chunkX + (- mod(chunkX, globalRegionSize) - regionOffset + i%regionSize)*ratio);
+ int fakeChunkZ = (int)(chunkZ + (- mod(chunkZ, globalRegionSize) - regionOffset + i/regionSize)*ratio);
+
+ miniRegionMap[i] = regionMap[getOutBoundX(fakeChunkX, regionToChunkRatio) + regionToChunkRatio*getOutBoundX(fakeChunkZ, regionToChunkRatio)];
+ }
+ return miniRegionMap;
+ }
+ public int getRiverHeight(int worldX, int worldZ, int min_height_custom, int max_height_custom, float data_width_per_pixel, SKBitmap height_map){
+ /*
+ Takes the river height at the (worldX, worldZ) coordinates.
+
+ Returns the minimum within a radius, to prevent artifacts at the water surface.
+ */
+ int radius = 2;
+ int[] localRiverHeights = new int[(2*radius+1)*(2*radius+1)];
+
+ for(int i=-radius; i 0){
+ localRiverHeights[i+radius+(j+radius)*(2*radius+1)] = (int) (min_height_custom + (max_height_custom - min_height_custom) * LerpPosHeight(worldX + i, worldZ + j, 0, data_width_per_pixel, height_map));
+ }
+ else{
+ localRiverHeights[i+radius+(j+radius)*(2*radius+1)] = max_height_custom;
+ }
+ }
+ }
+
+ return localRiverHeights.Min();
+ }
+
+ public void makeLakes(IServerChunk[] chunks, int chunkX, int chunkZ, int chunksize, int waterID, int gravelID, int min_height_custom, int max_height_custom, float data_width_per_pixel, SKBitmap height_map){
+ /*
+ Fills the potential lakes in the given chunk.
+ */
+ int lakeHeight;
+ int groundHeight;
+
+ for (int lZ = 0; lZ < chunksize*chunksize; lZ++){
+ int worldX = chunkX * chunksize + lZ%chunksize+ offsetX;
+ int worldZ = chunkZ * chunksize + lZ/chunksize + offsetZ;
+
+ groundHeight = (int) (min_height_custom + (max_height_custom - min_height_custom) * LerpPosHeight(worldX, worldZ, 0, data_width_per_pixel, height_map));
+ lakeHeight = (int) (min_height_custom + (max_height_custom - min_height_custom) * LerpPosHeight(worldX, worldZ, 1, data_width_per_pixel, height_map));
+
+ if(lakeHeight > groundHeight){
+ for(int posY = groundHeight; posY < lakeHeight; posY ++){
+ setBlockId(lZ%chunksize, posY, lZ/chunksize, chunksize, chunks, waterID, fluid:true);
+ }
+
+ setBlockId(lZ%chunksize, groundHeight - 1, lZ/chunksize, chunksize, chunks, gravelID);
+ setBlockId(lZ%chunksize, groundHeight - 2, lZ/chunksize, chunksize, chunks, gravelID);
+
+ for(int posY = lakeHeight; posY < 30 + lakeHeight; posY ++){
+ SetBlockAir(lZ%chunksize, posY, lZ/chunksize, chunksize, chunks);
+ }
+ }
+ }
+ }
+ public void print(string str){
+ /**
+ Just a print
+ */
+ api.Logger.Notification(str);
+ }
+}
+struct ColumnResult
+{
+ public BitArray ColumnBlockSolidities;
+}
\ No newline at end of file
diff --git a/code_mods/WorldGen_AlpineStory/build.ps1 b/code_mods/WorldGen_AlpineStory/build.ps1
new file mode 100644
index 00000000..6be84e30
--- /dev/null
+++ b/code_mods/WorldGen_AlpineStory/build.ps1
@@ -0,0 +1,2 @@
+dotnet run --project CakeBuild/CakeBuild.csproj -- $args
+exit $LASTEXITCODE;
\ No newline at end of file
diff --git a/code_mods/WorldGen_AlpineStory/build.sh b/code_mods/WorldGen_AlpineStory/build.sh
new file mode 100644
index 00000000..41f254aa
--- /dev/null
+++ b/code_mods/WorldGen_AlpineStory/build.sh
@@ -0,0 +1 @@
+dotnet run --project ./CakeBuild/CakeBuild.csproj -- "$@"