Map Scripting?

MadMage86

Warlord
Joined
Sep 24, 2009
Messages
127
Location
Florida
Are there any experts on map scripts floating around here still? I'd like to modify some of the standard map scripts for multiplayer games but I'm getting nowhere trying to add things to the core scripts - they all seem to keep calling to functions off-script that I can't see, so I've no clue how they work. If anyone's got tips on figuring out scripting, I'd be much obliged. Thanks.
 
Are there any experts on map scripts floating around here still? I'd like to modify some of the standard map scripts for multiplayer games but I'm getting nowhere trying to add things to the core scripts - they all seem to keep calling to functions off-script that I can't see, so I've no clue how they work. If anyone's got tips on figuring out scripting, I'd be much obliged. Thanks.

try this script. it will help you.
http://forum.civilization.org.pl/download.php?id=1572
 
I'm interested primarily in generating more rivers and lakes in any given map; I'd like to have a modular code I could just drop in and go with.

I'd also like a function that would generate long, thin mountain ranges. Thinking of something like the Rockies or the Himalayas, both of which create 'valleys' of sorts that would isolate players without having to rely on islands.
 
All right, so I have one other thing I'd like to try, but this one's harder. I'd like a variable continent generator that can create a variable number of landmasses and distributes players evenly among them - anything from one island per player to everyone on a single landmass. But here's the rub; I want to force all starting positions to coastal starts so I'd like to be able to take select civilizations (ones with an ocean bias, or perhaps ones who were historically islanders - Japan, for example) and put them on an isolated island regardless of any other settings.

Problem is I'm no expert at reading these .lua files and I'm having trouble finding out how even the standard map generators do basic functions. I wish coders were better at notation.
 
Actually, Sirian not only did an incredible job with the way random maps are generated and how assignments are made to them, but also in the way he explains things and notes items in the files. I'm certainly no programmer, and without them, I probably would have never grown a little more familiar with Lua and how it's utilized in this game. ;)

Anyway, as requested in the Steam Workshop, I'll try and give a brief explanation of the river and lake functions, even though I haven't really dealt with them in my own mods, so that you can modify them the way you like.

Code:
function AddRivers()
	
	print("Map Generation - Adding Rivers");
	
	local passConditions = {
		function(plot)
			return plot:IsHills() or plot:IsMountain();
		end,
		
		function(plot)
			return (not plot:IsCoastalLand()) and (Map.Rand(8, "MapGenerator AddRivers") == 0);
		end,
		
		function(plot)
			local area = plot:Area();
			local plotsPerRiverEdge = GameDefines["PLOTS_PER_RIVER_EDGE"];
			return (plot:IsHills() or plot:IsMountain()) and (area:GetNumRiverEdges() <	((area:GetNumTiles() / plotsPerRiverEdge) + 1));
		end,
		
		function(plot)
			local area = plot:Area();
			local plotsPerRiverEdge = GameDefines["PLOTS_PER_RIVER_EDGE"];
			return (area:GetNumRiverEdges() < (area:GetNumTiles() / plotsPerRiverEdge) + 1);
		end
	}
	
	for iPass, passCondition in ipairs(passConditions) do
		
		local riverSourceRange;
		local seaWaterRange;
			
		if (iPass <= 2) then
			riverSourceRange = GameDefines["RIVER_SOURCE_MIN_RIVER_RANGE"];
			seaWaterRange = GameDefines["RIVER_SOURCE_MIN_SEAWATER_RANGE"];
		else
			riverSourceRange = (GameDefines["RIVER_SOURCE_MIN_RIVER_RANGE"] / 2);
			seaWaterRange = (GameDefines["RIVER_SOURCE_MIN_SEAWATER_RANGE"] / 2);
		end
			
		for i, plot in Plots() do 
			if(not plot:IsWater()) then
				if(passCondition(plot)) then
					if (not Map.FindWater(plot, riverSourceRange, true)) then
						if (not Map.FindWater(plot, seaWaterRange, false)) then
							local inlandCorner = plot:GetInlandCorner();
							if(inlandCorner) then
								DoRiver(inlandCorner);
							end
						end
					end
				end			
			end
		end
	end		
end

There's 4 types of pass conditions. For each one, the function loops through all plots on the map to find spots to run DoRiver and generate a river. The first pass condition simply checks for a hill or mountain plot. The second pass condition checks for non-coastal plots and only chooses them about 12.5% of the time.

The third and fourth pass conditions use the PLOTS_PER_RIVER_EDGE XML value (the third one also only looks for hills or mountains), though I'm not "exactly" sure how GetNumRiverEdges functions (I'm still not too familiar with C++, and this one I can't figure out what it's doing exactly). So, these two I'm not too sure of. Though, by just looking at it, it looks like you can decrease the PLOTS_PER_RIVER_EDGE value to make the pass condition "true" more often since it's part of the denominator and will make the value to the right of the "<" symbol larger more often.

So, that's one thing you can try and tinker with to increase the amount of riverside tiles on the map: decrease PLOTS_PER_RIVER_EDGE

Next you have two more XML values: RIVER_SOURCE_MIN_RIVER_RANGE and RIVER_SOURCE_MIN_SEAWATER_RANGE

The last two pass conditions only use 1/2 the value.

The main loop which iterates through all plots starts by making sure the plot isn't water, then it checks the plot for the pass condition the loop is on.

Then, it uses RIVER_SOURCE_MIN_RIVER_RANGE to make sure there's no fresh water within a radius equal to its value. Then, it uses RIVER_SOURCE_MIN_SEAWATER_RANGE to make sure there's no salt water within a radius equal to its value (it actually checks for both, but fresh water has already been checked in the upper part of the loop). Then if the plot has an "inland corner" (the point of the hex towards the southeast, and shared with other adjacent land tiles), it runs the DoRiver function, which repeats itself while "flowing" the river between hexes.

Decreasing these range values would make the FindWater function return "false" more often (which would ultimately make the check "true" because of the "not" in front of it), which would make it run DoRiver more often.

So, those are two more things you can tinker with: decrease RIVER_SOURCE_MIN_RIVER_RANGE and decrease RIVER_SOURCE_MIN_SEAWATER_RANGE

I would imagine the result of decreasing some of these values would be that you'd have more rivers, but they could potentially be shorter than the default ones.

- - - - - - - -

Increasing lakes is pretty easy, but is still random. Also, luckily, lakes are placed after rivers, so it won't interfere with river generation.

Code:
function AddLakes()

	print("Map Generation - Adding Lakes");
	
	local numLakesAdded = 0;
	local lakePlotRand = GameDefines.LAKE_PLOT_RAND;
	for i, plot in Plots() do
		if not plot:IsWater() then
			if not plot:IsCoastalLand() then
				if not plot:IsRiver() then
					local r = Map.Rand(lakePlotRand, "MapGenerator AddLakes");
					if r == 0 then
						plot:SetArea(-1);
						plot:SetPlotType(PlotTypes.PLOT_OCEAN);
						numLakesAdded = numLakesAdded + 1;
					end
				end
			end
		end
	end
	
	-- this is a minimalist update because lakes have been added
	if numLakesAdded > 0 then
		print(tostring(numLakesAdded).." lakes added")
		Map.CalculateAreas();
	end
end

It uses the XML value LAKE_PLOT_RAND to set the range of its random roll. A roll of 0 is a hit and places a lake.

So, to increase the chances of more lakes, decrease LAKE_PLOT_RAND.
- - - - - - - - - - -

Outside of those simple XML tweaks, perhaps you may want to rewrite those Lua functions to increase things exactly the way you want; mainly the river ones. Hopefully, the explanation of what's going on with them helps you understand it a bit, and hopefully, I also accurately described things! Perhaps there are other, more advanced modders/programmers here that can lend you a better hand.
 
Problem is I have to be able to do all of this within the .lua file - I can't mess with the .xml's because that would require loading the script as a 'mod', thus eliminating the multiplayer capabilities (and I only want the script for multiplayer purposes).

Also, as far as lakes going after rivers - I'd like to try changing that. The reasoning behind running rivers first was so that lakes wouldn't cause rivers to 'end prematurely', but the reality of geography is that a river is often what 'feeds' a lake, so it's odd to not see one flowing into a lake. What I'm thinking I'd like to see happen is a script that generates 'bowls' of mountains (not complete circles, mind you) with lake formations near the center and rivers flowing down from the mountains to 'pool' in the valley below the way a snowmelt might, for example. It would be cool to give a more realistic climate generation that uses a 'trade winds' script to put a 'dry, arid' region on one side of a mountain range and a lush, forested area on the other side with lots of lakes and rivers; this would account for the rain coming in from the coast, getting picked up by the trade winds and getting caught by the mountain ranges and flowing down into the valley, effectively making the valley on the other side devoid of regular rainfall.

I actually modified your map script to be usable in multiplayer games and had to bring every call to an .xml file back to the script itself; we did have some issues with the lack of hills and mountains; mountains are useful for observatories and it's hard enough as is to find a good mountain location without limiting it to two per map, and the lack of hills makes production a massive problem. As a side effect I've also noticed that your script generates very few rivers and lakes.
 
Problem is I have to be able to do all of this within the .lua file - I can't mess with the .xml's because that would require loading the script as a 'mod', thus eliminating the multiplayer capabilities (and I only want the script for multiplayer purposes).

Hmm... okay, you can use a quick and dirty method if you wish. Just look up the XML values and replace the call to them with your own hard value.

Example:

local plotsPerRiverEdge = GameDefines["PLOTS_PER_RIVER_EDGE"]

becomes...

local plotsPerRiverEdge = 12


MadMage86 said:
Also, as far as lakes going after rivers - I'd like to try changing that. The reasoning behind running rivers first was so that lakes wouldn't cause rivers to 'end prematurely', but the reality of geography is that a river is often what 'feeds' a lake, so it's odd to not see one flowing into a lake. What I'm thinking I'd like to see happen is a script that generates 'bowls' of mountains (not complete circles, mind you) with lake formations near the center and rivers flowing down from the mountains to 'pool' in the valley below the way a snowmelt might, for example. It would be cool to give a more realistic climate generation that uses a 'trade winds' script to put a 'dry, arid' region on one side of a mountain range and a lush, forested area on the other side with lots of lakes and rivers; this would account for the rain coming in from the coast, getting picked up by the trade winds and getting caught by the mountain ranges and flowing down into the valley, effectively making the valley on the other side devoid of regular rainfall.

That will take a lot of rewriting of the code to achieve; very ambitious! What you wrote reminded me of features within another script I've seen here. If realism is your cup of tea, perhaps you should check out this nifty script and utilize some of its code: cephalo's PerfectWorld3.

MadMage86 said:
I actually modified your map script to be usable in multiplayer games and had to bring every call to an .xml file back to the script itself; we did have some issues with the lack of hills and mountains; mountains are useful for observatories and it's hard enough as is to find a good mountain location without limiting it to two per map, and the lack of hills makes production a massive problem. As a side effect I've also noticed that your script generates very few rivers and lakes.

That's good; yeah, the XML part isn't that important in that mod. It's just mostly text changes and changing of player/cs totals (which can be done manually).

Yeah, I agree. I too believe that I cut the mountains and hills a little much in that old script. There's lots of things that I would change now when I look back it at. :D

You can easily increase them again by applying an "extra_mountains" value of 10 or more. That will lower the required height of the fractal values for mountains and foothills by another 10 percent. You can also double the adjustment to foothills again so that it clumps more hills around mountains. Other than that, and increasing the grain by +1, I don't think I did anything else to it.

As for rivers and lakes, I didn't do anything to those. With rivers, perhaps it's simply due to the nature of this type of map; more landmasses separated by water, which creates less room for radius checks to avoid water. On pangaea or continents maps, there's probably more rivers.

Also, I made this script before one of the patches which updated and further improved the AI's use of maps with lots of different landmasses. So, at the end of the file, you'll want to update this:

-- tell the AI that we should treat this as a naval expansion map
Map.ChangeAIMapHint(1);

to this...

-- tell the AI that we should treat this as a offshore expansion map
Map.ChangeAIMapHint(4);

(I included the comments for reference.)

That'll give the AI better instructions to play on this script.
 
See, my problem is I don't know where to put that; as I understand, the call to the .xml exists in the 'mapgenerator' .lua file - how and where would I change the call within my own script? I understand that it's being loaded an the functions are just being called by my script, but I'm looking at, for example, the standard pangaea script and it doesn't even really seem to 'run' any of those functions. It just defines a function or two of it's own and passes them a few variables. I may just be sounding ******ed here, I'm used to LISP...

I've been modifying Cephalo's PerfectWorld scripts since Civ 4, actually. I found it odd that he chose to create landmasses that form around mountain ridges because I recall him distinctly saying that he disliked the way it looked on his Civ 4 version.

In any event, Cephalo's script is amazingly convoluted and the only thing I really like that it does is those mountain ridges - the problem is it very often gives you long snaking pangaeas that just loop across the equator in a giant circle; the mountains generate nicely, but there isn't much land that isn't 'coast next to a mountain' and his river generation is very much 'feast or famine' - tons in the jungle regions and no rivers or forests where a temparate climate should be. I'd been nagging him for a bit to refine the code but he vanished ages ago; I had to make modifications of my own just to make the script usable in a multiplayer setting, and even to this day I find his list of variables you can change at the beginning of the script to be mind boggling - some do nothing, while others make such odd, vast changes that it's hard to tell what the point of making them variable at all was.
 
Perhaps changing the modifier that makes sure there isn't an ocean in a certain radius would work; generally my gripe is that there aren't any rivers on smaller landmasses anyway, so if you're on a peninsula or island you end up with no rivers. I imagine the reason behind this is because rivers are generally considered an 'inland' option for gold generation, but rivers are also tied to some other buildings (gardens and hydro plants, for example) that provide such massive bonuses to certain playstyles that cities without river access are gimped.
 
Basically, the way I understand it is:

MapGenerator.lua contains the core function to generate maps: GenerateMap()

Within that, the other main functions are called, such as the massive StartPlotSystem(). All those functions are within the file, but some are just "dummy" ones that should be overridden by each map script, such as GenerateTerrain().

For example, many of the default map scripts include FractalWorld.lua to handle initial plot fractal generation, plot shifting, and plot assignments (ocean, land, hill, or mountain); TerrainGenerator.lua for terrain assignments (grass, plains, etc.); and FeatureGenerator.lua for feature assignments (forests, jungle, ice, atolls, etc.). MapGenerator.lua is included to handle any other functions leftover, like AddRivers() and AddLakes(), and also through MapGenerator, AssignStartingPlots.lua is included.

Any functions within your main map script will override the ones found in the other included Lua files. So, for example, if you want to tweak the AddRivers() function, just copy the whole thing into your map script file and rewrite the code and/or tweak values.

Though, if you're just tweaking values and the function you're modifying accepts arguments (args) or anything else, then you can also just add your changes there when the function is called instead of copying the whole thing into your script. This probably has the advantage of being more resilient to "breaking" when new patches come out and modify things in functions.

An example within SmallContinents.lua:

Code:
local args = {
	sea_level = sea_level,
	world_age = world_age,
	sea_level_low = 69,
	sea_level_normal = 75,
	sea_level_high = 80,
	extra_mountains = 10,
	adjust_plates = 1.5,
	tectonic_islands = true
	}
local plotTypes = fractal_world:GeneratePlotTypes(args);

It doesn't include the whole GeneratePlotTypes function in FractalWorld and instead just passes modified arguments to it (anything not included will be set equal to a default value within the function).

So again, just copy the whole function in and modify it (when in doubt, this is probably the best way), or just pass different values to it (if it accepts them) like the above example.

Also, I'm not sure of the exact programming terminologies or if I'm saying things the right way, but I hope that helps you understand it a bit more. :)
 
That actually helps a ton; I hadn't realized it just 'overrides' things within the basic script. FIguring out whihc .xml's the base code is looking at and changing the values may give me plenty of options as is.

The real beast is messing with the way landmasses are generated; I've been stumped for ages with the fractal generator and writing new variants from scratch just seems daunting.

[EDIT]: Hey, what does the 'grain' variable do, exactly?
 
Grain controls the clustering of things. High grain values makes things spread out more; low grain values clump them together more.

One example is with landmasses. A continent grain of 2 will generally produce continents, a grain of 3 will generally produce more continents that are smaller, a grain of 4 will generally produce archipelago, and a grain of 5 will generally produce many tiny islands.

Here's an old post of mine when I wasn't very knowledgeable of fractals or what "heights" exactly were: http://forums.civfanatics.com/showthread.php?t=484233

A fractal is just a matrix of values, but I believe the numbers aren't just a random mess. Instead, they work off of one another and steadily decrease or increase relative to adjacent points (I believe), so that if you blanketed them all with a plane, you get an image like this:



I would imagine the way grain plays into these fractals is that it makes those increases and decreases between points more or less steep. So, a fractal with a low grain would be a smoother plane with ridges that have gradual slopes. A fractal with a high grain would be spikier with ridges spread-out and with steeper slopes.

In that old thread, the idea clicked when I thought of it as a 3D plane inside of an aquarium, lol. So when you obtain the height value you're looking for, you mark it on the side of the aquarium and fill it with an opaque liquid up to the line. Now, when you look straight down at the plane from above, you'll see the ridges which protrude out of the water and pass the height test. So, if it was a height value applied to a forest fractal, all those ridges that are still visible would be forest.

Though, there are other things to consider. Say that was marsh, for example. Marsh usually has a height of 92 (or 8% marsh on maps). But, it really isn't 8%. The fractal doesn't know that, and it surely isolates 8% of the relative height of the fractal values. But, when you apply it to a map under construction, other things play in. Such as, mountains and hills already placed on grassland tiles that could potentially pass the height test; they block marsh.

The same can be said for jungles and forests, which are placed on grassland tiles (note: all jungle is later converted to plains using a final adjustment function). Marsh is placed before them, so even though it's a small amount, it blocks-out some of the forests and jungle. If one were to dramatically increase marsh percentages, you would probably want to have them placed in a different order, behind forest and jungle; unless marshes are that important to one's script.

Hope that helps. :)

EDIT: Oh... and as for the fractal values, I believe they work off of a value range of 0 (or 1?) - 255 (or 256?). As I tinkered with some print outs to get information while testing things with my script and that seemed to be the general range (on Standard size anyway... not sure if it is relative to map size, but I very much doubt it). An example can be seen here which may prove this:

Code:
function TerrainGenerator:GetLatitudeAtPlot(iX, iY)
	-- Terrain bands are governed by latitude.
	-- Returns a latitude value between 0.0 (tropical) and 1.0 (polar).
	local lat = math.abs((self.iHeight / 2) - iY) / (self.iHeight / 2);
	
	-- Adjust latitude using self.variation fractal, to roughen the border between bands:
	[COLOR="red"]lat = lat + (128 - self.variation:GetHeight(iX, iY))/(255.0 * 5.0);[/COLOR]
	-- Limit to the range [0, 1]:
	lat = math.clamp(lat, 0, 1);
	
	return lat;
end

Terrain doesn't use a hard latitude calculation, or else it would look horrible and the band lines would be too straight and unnatural looking (I've tested things with no variation while reworking the default terrain bands, so I get them precisely the way I wanted before reapplying the variation again). Instead, it applies a variation to the lat so the terrain checks think the plot is actually higher or lower than it really is.

Using the formula, highlighted in red, one can see that it creates a variation value roughly between -0.1 and 0.1 (equal to 4 rows on a Huge map, since they're each 0.025).

Using the extreme ends of the fractal:

(128 - 0) / (255 * 5) = 0.1003

(128 - 255) / (255 * 5) = -0.0996

If you wanted to decrease the variation you could multiply the denominator by 10 instead of 5, which would halve the results to a range of only roughly -0.05 and 0.05.

I customized these calculations to multiple variation methods which apply to different terrain bands. Some will favor more blending towards the inner part of the map and restrict blending towards the outer parts, other terrains will favor blending towards the outer parts of the map and restrict inner blends more. It comes out really nice.


- - - - - - - - - - - - - - - - - - - - - - -


Here's another little hint, which I've applied to my script:

It's very minor, but latitude is off on all maps and latitude checks. Since all maps have even number heights and a single inner row set to 0, you never get horizontally symmetrical maps.

Using the default formula:

Code:
local lat = math.abs((self.iHeight / 2) - iY) / (self.iHeight / 2)

The bottommost row will always be 1.0 and the topmost row will never be 1.0. On a Huge map, the topmost row would be 0.975. On a Standard map, the topmost row would be only 0.963... it gets worse on smaller maps. You can see an example of this by looking at ice formations at the bottom of the map, they'll always have an additional row of ice. (It drove me crazy until I finally understood and figured this all out.)

I use this method when calculating latitude:

Code:
local lat = 0;
if (iY >= (self.iHeight/2)) then
	lat = math.abs((self.iHeight/2) - iY)/(self.iHeight/2);
else
	lat = math.abs((self.iHeight/2) - (iY + 1))/(self.iHeight/2);
end

With this, you get a horizontally symmetrical and balanced map. Within the equator, you'll have two rows equal to 0 instead of only one.

To be fair, the default method may be intentional due to the viewing angle of the game's world. But, I personally feel it isn't necessary and it looks just fine with my modded method (and I'm VERY picky!), especially since it's just ice down there. Plus, think about it, what would you rather have: another row of ice or another row of useful equatorial land? :D
 
Top Bottom