Alright, something is changing tiles that I have in my Land Table. Haven't figured out if it's happening before or after feature generation (most likely before). Should just have to add a check to make sure I'm dealing with land. A big part of why using the look-up tables is faster is you get to skip so many checks. I'm pretty sure the AddLakes() function is responsible... Stupid AddLakes()!
I've gotten some of the issues with my Tectonics implementation worked out. Right now I'm working on preventing continents that span from pole to pole (and thereby cutting off circumnavigation via sea). I'm trying to write some fairly simple code to make a table that catalogs what plates are neighboring each other and I have no earthly idea what's going on... That's what happens when you take a 5 month break though. On top of that, WB is bugging out after 7 or 8 maps generated and putting Old Faithful on every single tile! It doesn't stop the geyser spam unless I restart WB, so it's been pretty slow-going.
The good news is I now spawn a huge map (128x80) in ~12 seconds in WB.
Heh, saw the geyser bug in the sticky thread. Is that still in the July beta? I must exit just in time
Honestly, since replacing the lua compiler as you suggested, huge map roll time in WB is a complete non-issue. It varies slightly, but on my desktop it might take 6 seconds (I count real slow though ). Now if it would just remember the damned settings between maps...
The only trouble I had briefly with LuaJIT (so far) was having to hunt for some syntax nitpicks it had with some of Gedemon's mods. He was quick to point out saveutils.lua though, which seems to have been the only culprit (in my case).
Keep it up with your tectonics project. Can't wait to see the results
I actually built a stop watch into the script. It operates on os.clock() so it's got a resolution of something like 30ms, but that's fine for what I'm using it for.
Back up to ~16 seconds; ensuring that flipping a plate won't blockade sea travel or create a super-continent takes a lot of ops. I've got a lot done. Fixed the issue with features appearing on lakes. It was happening because of the default AddLakes() function so I just brought it in and re-tooled it to use my land table. Shaved about 300ms off of map generation and fixed a bug at the same time.
The worse news is that my changes to landmass generation don't scale very well and about 1:4 Duel size maps (42x26 I think) crash with the "Endless loop in Lake siltification" error. Probably just gonna see if I can remove Duel Size maps as I'm really not intending to make a good tiny map in the first place.
Here is a new version of Perfect World, based on the VEM (Vanilla Enhanced Mod by Thalassicus) version, which I called Perfect World 4.0.
I updated the file according to this thread, but did some other things as well:
It's awsome what you can do by tuning all those map constants.
My way to "reduce the stringy shapes" or "terrain stripes/lines" as I call them, is a bit more direct and not fully successful either. Hope you have fun with the additional custom options.
I personally just extracted his script with 7zip (.civ5mod files are just archives) and placed it at \Documents\My Games\Civ V(or whatever it is)\Maps\
Works fine installed that way. This is a common way of installing Map Scripts as it is generally more reliable than the mod browser (especially if you make alterations to the script since Steam may decide to re-download the mod whenever and that will overwrite your altered version) and it allows them to be used in Multiplayer.
Alright, I tried that version 4.11 and I think it's strange that it generates grass and forests along all rivers, even in deserts. This way there are no rivers like Egypt or Mesopotamia. Otherwise it's a good map.
I believe I have written the fastest spiral iterators known to CiV... I got my script running in ~9.6 seconds (in WorldBuilder, default lua.dll [not the JIT.dll, which drops the run time down to ~5.6 seconds]). I was running the script in ~16 seconds using Cephalo's spiral. I subbed in Plot_GetPlotsInCircleSimple() from the communitas script and cut it down to ~12.
I then looked at the results Plot_GetPlotsInCircleSimple() was returning and noticed it was making a rectangle, not a spiral! So, I set out to write a faster iterator that actually returned a spiral. I knew I'd need to simplify things, so I decided to avoid function calls, c++ method calls, etc. I managed to do it by working with index values and using simple arithmetic (add and subtract).
Even though my spiral iterator was already faster than Plot_GetPlotsInCircleSimple() I decided to try to write a second one (that's even faster) using a scan method constrained to the edges of our given spiral instead of actually scanning outwards from the center with a spiral! This proved to be even faster, though it returns the results in a different order than a true spiral iterator (more on that later).
Both functions expect an index value as an input (not (x,y)) and they both return a table of index values (instead of a table of tables of (x,y) coordinates). Working with one index value is faster than working with two coordinates (especially when it comes to storing and reading from the returned table). Both functions assume xWrap so if you're trying to adapt them to a script that doesn't have xWrap or has yWrap (the two are exclusive), good luck! In the vast majority of cases, you can find a way around needing (x,y) as there's already a Map.GetPlotByIndex() method as well as alternative ways to determine certain things. If you absolutely must have x and/or y, either calculate them from the index (x = i%W; y = (i-x)/W) or wait for me to post my next function which will make lookup tables for x and y at initialization and use that to "lookup" the coordinate(s) you need.
Without further adieu, I present to you GetSpiral() and GetCircle():
Meet, GetSpiral(). It returns a table that "grows" outward from the center. If you're using the key values of the returned table to determine distance from the center plot, this is the function to use (see my PlacePossibleOasis() from the file I posted previously in this thread for an example of this).
Edit: Added functionality for minimum radius, and changed the table store so that it inserts "-1" into the table for plots that would be off of the map. If you're calling this, you need to check that i ~= -1 before doing anything with [like running Map.GetPlotByIndex()]. If you don't do this check, it's likely to cause a crash. This change ensures that keys n-nn are exactly z tiles away from the center (without using a minRadius, 1 is the tile itself, 2-7 are adjacent, 8-19 are 2 tiles away, 20-37...).
Spoiler:
Code:
function GetSpiral(i,maxRadius,minRadius)
local W,H = Map.GetGridSize()
local WH = W*H
local x = i%W
local y = (i-x)/W
local odd = y%2
local tab ={}
local first = true
if minRadius == nil or minRadius == 0 then
table.insert(tab,i)
minRadius = 1
end
for r = minRadius, maxRadius, 1 do
if first == true then
--start r to the west on the first spiral
if x-r > -1 then
i = i-r
x = x-r
else
i = i+(W-r)
x = x+(W-r)
end
first = false
else
--go west 1 tile before the next spiral
if x ~= 0 then
i = i-1
x = x-1
else
i = i+(W-1)
x = W-1
end
end
--Go r times to the NE
for z=1,r,1 do
if x ~= (W-1) or odd == 0 then
i = i+W+odd
x = x+odd
else
i = i + 1
x = 0
end
--store the index value or -1 if the plot isn't on the map; flip odd
if i > -1 and i < WH then table.insert(tab,i) else table.insert(tab,-1) end
if odd == 0 then odd = 1 else odd = 0 end
end
--Go r times to the E
for z=1,r,1 do
if x ~= (W-1) then
i = i+1
x = x+1
else
i = i-(W-1)
x = 0
end
--store the index value or -1 if the plot isn't on the map
if i > -1 and i < WH then table.insert(tab,i) else table.insert(tab,-1) end
end
--Go r times to the SE
for z=1,r,1 do
if x ~= (W-1) or odd == 0 then
i = i-W+odd
x = x+odd
else
i = i-(2*W-1)
x = 0
end
--store the index value or -1 if the plot isn't on the map; flip odd
if i > -1 and i < WH then table.insert(tab,i) else table.insert(tab,-1) end
if odd == 0 then odd = 1 else odd = 0 end
end
--Go r times to the SW
for z=1,r,1 do
if x ~= 0 or odd == 1 then
i = i-W-1+odd
x = x-1+odd
else
i = i-(W+1)
x = (W-1)
end
--store the index value or -1 if the plot isn't on the map; flip odd
if i > -1 and i < WH then table.insert(tab,i) else table.insert(tab,-1) end
if odd == 0 then odd = 1 else odd = 0 end
end
--Go r times to the W
for z = 1,r,1 do
if x ~= 0 then
i = i-1
x=x-1
else
i = i+(W-1)
x = (W-1)
end
--store the index value or -1 if the plot isn't on the map
if i > -1 and i < WH then table.insert(tab,i) else table.insert(tab,-1) end
end
--Go r times to the NW!!!!!
for z = 1,r,1 do
if x ~= 0 or odd == 1 then
i = i+W-1+odd
x = x-1+odd
else
i = i+W+(W-1)
x = W-1
end
--store the index value or -1 if the plot isn't on the map; flip odd
if i > -1 and i < WH then table.insert(tab,i) else table.insert(tab,-1) end
if odd == 0 then odd = 1 else odd = 0 end
end
end
return tab
end
Meet, GetCircle() this one is the scan method so it returns a table with the plots sorted ascending by y and then (kindof) by x. Use this function any time that distance from the center doesn't matter. It's performance improvement is exaggerated by how big of a radius you're looking at so functions like Smooth() or Deviate() (which call this function with a pretty large radius once for every single plot on the map) see the most benefit.
Spoiler:
Code:
function GetCircle(i,radius)
local W,H = Map.GetGridSize()
local WH = W*H
local x = i%W
local xx = x
local y = (i-xx)/W
local odd = y%2
local tab = {}
local topY = radius
local bottomY = radius
local currentY = nil
local len = 1+radius
--constrain the top of our circle to be on the map
if y+radius > H-1 then
for r=0,radius,1 do
if y+r == H-1 then
topY = r
break
end
end
end
--constrain the bottom of our circle to be on the map
if y-radius < 0 then
for r=0,radius,1 do
if y-r == 0 then
bottomY = r
break
end
end
end
--adjust starting length, apply the top and bottom limits, and correct odd for the starting point
len = len+(radius-bottomY)
currentY = y - bottomY
topY = y + topY
odd = (odd+bottomY)%2
--set starting point
if x-(radius-bottomY)-math.floor((bottomY+odd)/2) < 0 then
i = i-(W*bottomY)+(W-(radius-bottomY))-math.floor((bottomY+odd)/2)
x = x+(W-(radius-bottomY))-math.floor((bottomY+odd)/2)
-- print(string.format("i for (%d,%d) WOULD have been in outer space. x is (%d,%d) i is (%d)",xx,y,x,y-bottomY,i))
else
i = i-(W*bottomY)-(radius-bottomY)-math.floor((bottomY+odd)/2)
x = x-(radius-bottomY)-math.floor((bottomY+odd)/2)
end
--cycle through the plot indexes and add them to a table
--local str = ""
--local iters = 0
while currentY <= topY do
--insert the start value, scan left to right adding each index in the line to our table
--str = str..i..","
table.insert(tab,i)
local wrapped = false
for n=1,len-1,1 do
if x ~= (W-1) then
i = i + 1
x = x + 1
else
i = i-(W-1)
x = 0
wrapped = true
end
--str = str..i..","
table.insert(tab,i)
end
if currentY < y then
--move i NW and increment the length to scan
if not wrapped then
i = i+W-len+odd
x = x-len+odd
else
i = i+W+(W-len+odd)
x = x+(W-len+odd)
end
len = len+1
else
--move i NE and decrement the length to scan
if not wrapped then
i = i+W-len+1+odd
x = x-len+1+odd
else
i = i+W+(W-len+1+odd)
x = x+(W-len+1+odd)
end
len = len-1
end
currentY = currentY+1
if odd == 0 then odd = 1 else odd = 0 end
-- iters = iters+1
-- if iters > 300 then
-- print("infinite loop in GetCircle")
-- break
-- end
end
-- print(string.format("added "..str.." to table for circle starting at(%d,%d)",xx,y))
return tab
end
Edit: P.S.: If anyone knows of a faster way of determining "odd" I'd love you forever for sharing (even if it doesn't net any tangible performance difference)! "odd" is a value that determines whether or not the y coordinate we're currently at is even or odd. This value is then used to determine the "every-other-line" offset that affects x values in a hexagonal grid. I initially set odd by calculating y and then doing odd = y%2. I then continue tracking odd by running odd = (odd+1)%2 every time the y value I'm dealing with increments. This makes the modulus operator the only "complex" arithmetic in either function. I know with bitwise operators AND'ing any integer with 1 will tell you if it's even or odd; unfortunately, bitwise operators aren't built-in to lua and including a library that makes them available may affect the distributivity of my script.
I rewrote function floatMap:GetNeighbor() to take xWrap into consideration. This allows river pathing (and anything else that may use this function) to wrap properly and fixes a very rare crash related to river pathing (the script tries to analyze a plot that doesn't exist). Replace all of function floatMap:GetNeighbor() with the following code:
Spoiler:
PHP:
function FloatMap:GetNeighbor(x,y,dir)
local xx
local yy
local odd = y % 2
if dir == mc.C then
return x,y
elseif dir == mc.W then
if x == 0 and self.wrapX then
xx = self.width-1
yy = y
else
xx = x - 1
yy = y
end
return xx,yy
elseif dir == mc.NW then
if x == 0 and odd == 0 and self.wrapX then
xx = self.width-1
yy = y + 1
else
xx = x - 1 + odd
yy = y + 1
end
return xx,yy
elseif dir == mc.NE then
if x == self.width-1 and odd == 1 and self.wrapX then
xx = 0
yy = y+1
else
xx = x + odd
yy = y + 1
end
return xx,yy
elseif dir == mc.E then
if x == self.width-1 and self.wrapX then
xx = 0
yy = y
else
xx = x + 1
yy = y
end
return xx,yy
elseif dir == mc.SE then
if x == self.width-1 and odd == 1 and self.wrapX then
xx = 0
yy = y - 1
else
xx = x + odd
yy = y - 1
end
return xx,yy
elseif dir == mc.SW then
if x == 0 and odd == 0 and self.wrapX then
xx = self.width - 1
yy = y - 1
else
xx = x - 1 + odd
yy = y - 1
end
return xx,yy
else
error("Bad direction in FloatMap:GetNeighbor")
end
return -1,-1
end
Edit: This may need some more testing, though I'll leave it here for now. I recall rivers usually appearing to be fine before I implemented Xshift. On decent sized maps (standard and larger) with a normal amount of land (less than 40% or so), river wrapping should almost never be an issue. If this does have an issue, I'd still prefer it's use over a version without wrapping so it would require changes to be made to river pathing.
So, one of the issues causing the map to crash during generation revolved around siltifying lakes. The function responsible for this relies on the sea level threshold. However, because of the way this threshold was getting set, it was possible to return a sea level threshold of 0.0000000. This resulted in an infinite loop (that errors out around 100,000 iterations iirc).
To fix this issue, I altered FloatMap:FindThresholdFromPercent() to return a mathematically calculated threshold whenever it's possible for the original method to return a value of zero. By default, only sea level could return zero, so only sea level should be changed. However, you should find that the results are extremely similar, if not identical, to the original method (with the exclusion of one possible source of a crash).
To fix this issue, I ended up just adding an extremely small constant to any threshold that could possibly return zero. The results are essentially identical though it appears to exclude the possibility of forcing an endless loop in SiltifyLakes(). Even when the threshold gets set to the constant (because it tried to set the threshold to zero), the map spawns beautifully. I'm pretty confident this issue stems from normilization, but if this constant that doesn't have any side effects fixes the issue, I see no need to rewrite something that's going to have major implications in everything the script does.
Replace FloatMap:FindThresholdFromPercent() with:
Spoiler:
Code:
function FloatMap:FindThresholdFromPercent(percent, greaterThan, excludeZeros)
local mapList = {}
local percentage = percent * 100
local const = 0.0
if not excludeZeros then
const = 0.000000000000000001
end
if greaterThan then
percentage = 100-percentage
end
if percentage >= 100 then
return 1.01 --whole map
elseif percentage <= 0 then
return -0.01 --none of the map
end
for i=0,self.length-1,1 do
if not (self.data[i] == 0.0 and excludeZeros) then
table.insert(mapList,self.data[i])
end
end
table.sort(mapList, function (a,b) return a < b end)
local threshIndex = math.floor((#mapList * percentage)/100)
return mapList[threshIndex-1]+const
end
This will resolve one of the two possible crashes for SiltifyLakes(). The other crash revolves around the possibility for there to be lakes left over after the body of the function has run. I haven't really dug into why this happens nor do I have any plans to, ever. The lakes left behind are few and far between and I've yet to notice any problems with just commenting out the entire section of code that forces this crash. The offending code is everything after the "while" statement ends.
See here:
Spoiler:
Code:
function RiverMap:SiltifyLakes()
local lakeList = {}
local onQueueMapNorth = {}
local onQueueMapSouth = {}
local i = 0
for y = 0,self.elevationMap.height - 1,1 do
for x = 0,self.elevationMap.width - 1,1 do
onQueueMapNorth[i] = false
onQueueMapSouth[i] = false
if self:isLake(self.riverData[i].northJunction) then
Push(lakeList,self.riverData[i].northJunction)
onQueueMapNorth[i] = true
end
if self:isLake(self.riverData[i].southJunction) then
Push(lakeList,self.riverData[i].southJunction)
onQueueMapSouth[i] = true
end
i=i+1
end
end
local longestLakeList = #lakeList
local shortestLakeList = #lakeList
local iterations = 0
local debugOn = false
--print(string.format("initial lake count = %d",longestLakeList))
while #lakeList > 0 do
--print(string.format("length of lakeList = %d",#lakeList))
iterations = iterations + 1
if #lakeList > longestLakeList then
longestLakeList = #lakeList
end
if #lakeList < shortestLakeList then
shortestLakeList = #lakeList
--print(string.format("shortest lake list = %d, iterations = %d",shortestLakeList,iterations))
iterations = 0
end
if iterations > 50000 then
debugOn = true
end
if iterations > 50300 then
error(string.format("endless loop in lake siltification; check logs. seaLevelThreashold is %.20f",elevationMap.seaLevelThreshold))
end
local junction = Pop(lakeList)
local i = self.elevationMap:GetIndex(junction.x,junction.y)
if junction.isNorth then
onQueueMapNorth[i] = false
else
onQueueMapSouth[i] = false
end
if debugOn then
print(string.format("processing (%d,%d) N=%s alt=%f",junction.x,junction.y,tostring(junction.isNorth),junction.altitude))
end
local avgLowest = self:GetLowerNeighborAverage(junction)
if debugOn then
print(string.format("--avgLowest == %f",avgLowest))
end
if avgLowest < junction.altitude + 0.005 then --cant use == in fp comparison
junction.altitude = avgLowest + 0.005
if debugOn then
print("--adding 0.005 to avgLowest")
end
else
junction.altitude = avgLowest
end
if debugOn then
print(string.format("--changing altitude to %f",junction.altitude))
end
for dir = mc.WESTFLOW,mc.VERTFLOW,1 do
local neighbor = self:GetJunctionNeighbor(dir,junction)
if debugOn and neighbor == nil then
print(string.format("--nil neighbor at direction = %d",dir))
end
if neighbor ~= nil and self:isLake(neighbor) then
local i = self.elevationMap:GetIndex(neighbor.x,neighbor.y)
if neighbor.isNorth == true and onQueueMapNorth[i] == false then
Push(lakeList,neighbor)
onQueueMapNorth[i] = true
if debugOn then
print(string.format("--pushing (%d,%d) N=%s alt=%f",neighbor.x,neighbor.y,tostring(neighbor.isNorth),neighbor.altitude))
end
elseif neighbor.isNorth == false and onQueueMapSouth[i] == false then
Push(lakeList,neighbor)
onQueueMapSouth[i] = true
if debugOn then
print(string.format("--pushing (%d,%d) N=%s alt=%f",neighbor.x,neighbor.y,tostring(neighbor.isNorth),neighbor.altitude))
end
end
end
end
end
--print(string.format("longestLakeList = %d",longestLakeList))
--print(string.format("sea level = %f",self.elevationMap.seaLevelThreshold))
[COLOR="SeaGreen"]--[[Commented out this section because it's debug code. -Bobert13
local belowSeaLevelCount = 0
local riverTest = FloatMap:New(self.elevationMap.width,self.elevationMap.height,self.elevationMap.xWrap,self.elevationMap.yWrap)
local lakesFound = false
for y = 0,self.elevationMap.height - 1,1 do
for x = 0,self.elevationMap.width - 1,1 do
local i = self.elevationMap:GetIndex(x,y)
local northAltitude = self.riverData[i].northJunction.altitude
local southAltitude = self.riverData[i].southJunction.altitude
if northAltitude < self.elevationMap.seaLevelThreshold then
belowSeaLevelCount = belowSeaLevelCount + 1
end
if southAltitude < self.elevationMap.seaLevelThreshold then
belowSeaLevelCount = belowSeaLevelCount + 1
end
riverTest.data[i] = (northAltitude + southAltitude)/2.0
if self:isLake(self.riverData[i].northJunction) then
local junction = self.riverData[i].northJunction
print(string.format("lake found at (%d, %d) isNorth = %s, altitude = %f!",junction.x,junction.y,tostring(junction.isNorth),junction.altitude))
riverTest.data[i] = 1.0
lakesFound = true
end
if self:isLake(self.riverData[i].southJunction) then
local junction = self.riverData[i].southJunction
print(string.format("lake found at (%d, %d) isNorth = %s, altitude = %f!",junction.x,junction.y,tostring(junction.isNorth),junction.altitude))
riverTest.data[i] = 1.0
lakesFound = true
end
end
end
if lakesFound then
error("Failed to siltify lakes. check logs")
end
riverTest:Normalize()
]]-- -Bobert13[/COLOR]
-- riverTest:Save("riverTest.csv")
end
Edit: My mathematical threshold formula is no good for Perlin noise generated elevation maps. I'm working on a better formula for them (and hopefully it will work just as well for my tectonic generation). The reason why it's no good is normalization. With Perlin noise maps, there may be no high mountain spikes so when the elevation map gets normalized, the average height is higher than the current formula expects. Alternatively, if there is an extremely large height spike, the average becomes much lower than expected. With my tectonic generation, height spikes always happen within a controlled range so the formula works as intended every time.
Edit2: Turns out, no matter what I did, the idea of a mathematical approximation of (median*percent) was a bust due to statistical anomalies. I got a coefficient that could produce a threshold within a certain variance of the threshold pulled through the (median*percent) table operation. It was even decently accurate (give or take ~0.03 from a threshold value that typically ranged between 0.45 and 0.65 in ~90% of all tests) however, every now and then (I'd estimate around 2% of the time), it would be waaaaaaaaay off and cause a map that was either almost completely water or almost completely land.
I'm now generating a huge map in ~6 seconds (~2.5 with the JIT.dll)!
The first optimization is to PlacePossibleAtoll. It's really simple and cuts about 5 seconds off of map generation.
Replace:
Code:
local featureAtoll = nil
for thisFeature in GameInfo.Features() do
if thisFeature.Type == "FEATURE_ATOLL" then
featureAtoll = thisFeature.ID;
end
end
with:
Code:
local featureAtoll = GameInfo.Features.FEATURE_ATOLL.ID
That's it... 5 seconds right there.
The next optimization is regarding my favorite functions, FloatMap:Smooth() and FloatMap:Deviate(). However, the optimization is actually to the functions they call repeatedly; FloatMap:GetAverageInHex() and FloatMap:GetStdDevInHex(). The new versions expect an index value as a parameter instead of (x,y) coordinates. You can either track back from each function in the stack and alter the parameters to pass the index value of the plot along, or calculate the index from (x,y) with FloatMap:GetIndex() and remove my calculations for x,y from index.
I've optimized these two functions by inlining the spiral iterator and working with the values, as they come in, instead of, calling the spiral iterator (which stores the values to a table and the returns that table) and then working with the values in the table. This is literally getting rid of hundreds of thousands of table operations! It's no 5 second optimization or anything , but it is tangibly faster.
FloatMap:GetAverageInHex():
Spoiler:
Code:
function FloatMap:GetAverageInHex(i,radius)
local W,H = Map.GetGridSize()
local WH = W*H
local x = i%W
local y = (i-x)/W
local odd = y%2
local topY = radius
local bottomY = radius
local currentY = nil
local len = 1+radius
local avg = 0
local count = 0
--constrain the top of our circle to be on the map
if y+radius > H-1 then
for r=0,radius-1,1 do
if y+r == H-1 then
topY = r
break
end
end
end
--constrain the bottom of our circle to be on the map
if y-radius < 0 then
for r=0,radius,1 do
if y-r == 0 then
bottomY = r
break
end
end
end
--adjust starting length, apply the top and bottom limits, and correct odd for the starting point
len = len+(radius-bottomY)
currentY = y - bottomY
topY = y + topY
odd = (odd+bottomY)%2
--set the starting point, the if statement checks for xWrap
if x-(radius-bottomY)-math.floor((bottomY+odd)/2) < 0 then
i = i-(W*bottomY)+(W-(radius-bottomY))-math.floor((bottomY+odd)/2)
x = x+(W-(radius-bottomY))-math.floor((bottomY+odd)/2)
-- print(string.format("i for (%d,%d) WOULD have been in outer space. x is (%d,%d) i is (%d)",xx,y,x,y-bottomY,i))
else
i = i-(W*bottomY)-(radius-bottomY)-math.floor((bottomY+odd)/2)
x = x-(radius-bottomY)-math.floor((bottomY+odd)/2)
end
--cycle through the plot indexes and add them to a table
while currentY <= topY do
--insert the start value, scan left to right adding each index in the line to our table
avg = avg+self.data[i]
local wrapped = false
for n=1,len-1,1 do
if x ~= (W-1) then
i = i + 1
x = x + 1
else
i = i-(W-1)
x = 0
wrapped = true
end
avg = avg+self.data[i]
count = count+1
end
if currentY < y then
--move i NW and increment the length to scan
if not wrapped then
i = i+W-len+odd
x = x-len+odd
else
i = i+W+(W-len+odd)
x = x+(W-len+odd)
end
len = len+1
else
--move i NE and decrement the length to scan
if not wrapped then
i = i+W-len+1+odd
x = x-len+1+odd
else
i = i+W+(W-len+1+odd)
x = x+(W-len+1+odd)
end
len = len-1
end
currentY = currentY+1
if odd == 0 then
odd = 1
else
odd = 0
end
end
avg = avg/count
return avg
end
FloatMap:GetStdDevInHex():
Spoiler:
Code:
function FloatMap:GetStdDevInHex(i,radius)
local W,H = Map.GetGridSize()
local WH = W*H
local x = i%W
local y = (i-x)/W
local odd = y%2
local topY = radius
local bottomY = radius
local currentY = nil
local len = 1+radius
local avg = self:GetAverageInHex(i,radius)
local deviation = 0
local count = 0
--constrain the top of our circle to be on the map
if y+radius > H-1 then
for r=0,radius-1,1 do
if y+r == H-1 then
topY = r
break
end
end
end
--constrain the bottom of our circle to be on the map
if y-radius < 0 then
for r=0,radius,1 do
if y-r == 0 then
bottomY = r
break
end
end
end
--adjust starting length, apply the top and bottom limits, and correct odd for the starting point
len = len+(radius-bottomY)
currentY = y - bottomY
topY = y + topY
odd = (odd+bottomY)%2
--set the starting point, the if statement checks for xWrap
if x-(radius-bottomY)-math.floor((bottomY+odd)/2) < 0 then
i = i-(W*bottomY)+(W-(radius-bottomY))-math.floor((bottomY+odd)/2)
x = x+(W-(radius-bottomY))-math.floor((bottomY+odd)/2)
else
i = i-(W*bottomY)-(radius-bottomY)-math.floor((bottomY+odd)/2)
x = x-(radius-bottomY)-math.floor((bottomY+odd)/2)
end
--cycle through the plot indexes and add them to a table
while currentY <= topY do
--insert the start value, scan left to right adding each index in the line to our table
local sqr = self.data[i] - avg
deviation = deviation + (sqr * sqr)
local wrapped = false
for n=1,len-1,1 do
if x ~= (W-1) then
i = i + 1
x = x + 1
else
i = i-(W-1)
x = 0
wrapped = true
end
local sqr = self.data[i] - avg
deviation = deviation + (sqr * sqr)
count = count+1
end
if currentY < y then
--move i NW and increment the length to scan
if not wrapped then
i = i+W-len+odd
x = x-len+odd
else
i = i+W+(W-len+odd)
x = x+(W-len+odd)
end
len = len+1
else
--move i NE and decrement the length to scan
if not wrapped then
i = i+W-len+1+odd
x = x-len+1+odd
else
i = i+W+(W-len+1+odd)
x = x+(W-len+1+odd)
end
len = len-1
end
currentY = currentY+1
if odd == 0 then
odd = 1
else
odd = 0
end
end
deviation = math.sqrt(deviation/count)
return deviation
end
Attached you should find my updated version of PerfectWorld3. This is intended to look, play, and feel like PerfectWorld. The main constants have been reorganized and I added some notation, but all of the relevant ones are set to the default values.
ChangeLog:
-Implemented multiple fixes for "ghost tiles" and map seam issues, including Xshift.
-Improved Oasis Placement.
-Changed Marsh Placement.
-Fixed some issues with feature/terrain striping near the equator.
-Added optional terrain matching features.
-Prevented single-tile mountain islands.
-Implemented a multiplayer-ready random.
-Fixed an issue with threshold determination that sometimes caused an infinite loop.
-Removed a rare error status that forced a crash. (The expected result is a river that ends in the middle of a landmass, so if you find this and you can get me some information [particularly the random map seed which will require lua logging to be enabled] I'll look further into this. If you can't get me a random seed, but you have a configuration of mapConstants that produces this often, that information could also be helpful.)
-Implemented a vast number of optimizations. (I spawn a 128x80 map in ~8.5 seconds with this script, where it takes ~70 with the original).
-Probably 40 other things I can't recall right now.
This will probably end my support for PerfectWorld3 as I'll be working on my derivative that uses an alternative landmass generation method. If there's major bugs, or I ever figure out how to fix the very low possibility for rivers that can't path to a body of water, I'll be happy to do an update for those. Should Cephalo return to his script, he's more than welcome to use or discard any of the changes I've made for this version at his sole discretion.
One caveat for anyone playing with this script and used to Perfectworld's abundance of rivers... you'll probably want to raise mconst.riverPercent. With the removal of most of the coastal "shorties" you can afford to raise this quite a bit from the default (.19) without generating too many fertile areas. That's my brief impression anyway from studying a bunch of maps in the WB.
edit: ...so I'll be playing with a higher value there and will keep my eye out for the pathing issue you mention, which should be more frequent (if it occurs at all)
Changelog:
-Minor fix to my Spiral iterator. I wasn't calling it with a minRadius, but if I would have, it wouldn't have returned proper values.
-Minor fix to GetAverageInHex and GetStdDevInHex. I wasn't including the first value of every scanned line after the first line.
-Refined my threshold determination to only add the constant if returning zero is a possibility and lowered the constant significantly.
-Changed the main constant "YtoXRatio" to calculate it's value based on the map's dimensions. Landmasses should appear less stringy and less "vertically-skewed".
-Fixed the possibility for rivers that can't path to a body of water! Cephalo was excluding the possibility that a given junction (read "corner of a hex") could still be a lake after SiltifyLakes() had adjusted it once.
-Lowered the value SiltifyLakes() adjusts by during each iteration. This takes a fraction of a second longer, but it should produce more refined results.
I haven't tested any mostly-land maps to see if my adjustments trigger the maximum iteration limit of SiltifyLakes() but hopefully, it shouldn't. Besides, if you're playing 95% land you probably picked the wrong mapscript.
I left the error status check for SiltifyLakes in, though I changed it to print a warning to the log and allow the map to spawn anyways. After god-knows how many spawns, since I added a check during the body that makes sure the junction adjusted isn't still a lake, I haven't seen the error message once.
One caveat for anyone playing with this script and used to Perfectworld's abundance of rivers... you'll probably want to raise mconst.riverPercent. With the removal of most of the coastal "shorties" you can afford to raise this quite a bit from the default (.19) without generating too many fertile areas. That's my brief impression anyway from studying a bunch of maps in the WB.
edit: ...so I'll be playing with a higher value there and will keep my eye out for the pathing issue you mention, which should be more frequent (if it occurs at all)
I actually had a problem with meeting a quota for number of lakes spawned using the default value because it couldn't find any viable plots due to the rivers (and deserts; I exclude lakes from spawning in deserts because that's what oases are for). I had to put a break into my loop after so many attempts to prevent an infinite loop.
Turns out my fix for SiltifyLakes() works, but not because it's check for the possibility that the junction that just got altered is a lake... It's because it's checking that the junction on the opposite side of the hex is a lake!
Junctions on the opposite side of a hex should not affect each other in this manner!! Only the three junctions that touch the same hex-edges as the junction itself should have anything to do with this!! This is a sign that something else is fundamentally wrong with lake determination...
I discovered all of this while working on optimizing SiltifyLakes (which right now can crash v5a on a 28% land, huge, non-Terra map (I made Terra Maps spawn larger to give everyone some leg-room hopefully) because I set the iteration limit too low.
Tested and confirmed that the zip is not corrupted. Make sure you're using an up-to-date version of 7zip or WinRar. WinZip (last I knew) has not been updated in years and won't be able to handle LZMA2 (the compression method used).
The file was renamed so it shouldn't overwrite anything. The map was also renamed so it will appear as "PerfectWorld3 (v5a)" in-game. The extracted contents of this file belong in \My Documents\My Games\Sid Meier's Civilization V\Maps\.
I'm 99.9% sure that I've worked out SiltifyLakes now. I'm still not sure why my band-aid fix worked though I suppose it could just be that it greatly reduced the chance of leaving lakes behind. My newest version doesn't need the opposite side of the hex and I've yet to see a left-over lake after thousands of spawns on a 95% land, 0.4 river size, 128x80 map. I've seen as many as 3 million iterations for it to get the job done... but no lakes left behind.
I'm going to chase down a potential problem with math.random (according to what I've read, we may need to be pausing after seeding and/or purging the first random number rolled to prevent issues with maps with the same exact seed from rolling differently). Once I determine what or if anything needs to be done there, I'll clean up and post v5b (hopefully FINAL).
This site uses cookies to help personalise content, tailor your experience and to keep you logged in if you register.
By continuing to use this site, you are consenting to our use of cookies.