cloudlands.lua 153 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956
  1. local ALTITUDE = 200 -- average altitude of islands
  2. local ALTITUDE_AMPLITUDE = 40 -- rough island altitude variance (plus or minus)
  3. local GENERATE_ORES = false -- set to true for island core stone to contain patches of dirt and sand etc.
  4. local LOWLAND_BIOMES = false or -- If true then determine an island's biome using the biome at altitude "LOWLAND_BIOME_ALTITUDE"
  5. minetest.get_modpath("ethereal") ~= nil -- Ethereal has an alpine biome above altitude 40, so default to lowland biomes
  6. local LOWLAND_BIOME_ALTITUDE = 10 -- Higher than beaches, lower than mountains (See LOWLAND_BIOMES)
  7. local VINE_COVERAGE = 0.3 -- set to 0 to turn off vines
  8. local REEF_RARITY = 0.015 -- Chance of a viable island having a reef or atoll
  9. local TREE_RARITY = 0.08 -- Chance of a viable island having a giant tree growing out of it
  10. local PORTAL_RARITY = 0.04 -- Chance of a viable island having some ancient portalstone on it (If portals API available and ENABLE_PORTALS is true)
  11. local BIOLUMINESCENCE = false or -- Allow giant trees variants which have glowing parts
  12. minetest.get_modpath("glowtest") ~= nil or
  13. minetest.get_modpath("ethereal") ~= nil or
  14. minetest.get_modpath("glow") ~= nil or
  15. minetest.get_modpath("nsspf") ~= nil or
  16. minetest.get_modpath("nightscape") ~= nil or
  17. minetest.get_modpath("moonflower") ~= nil -- a world using any of these mods is OK with bioluminescence
  18. local ENABLE_PORTALS = true -- Whether to allow players to build portals to islands. Portals require the Nether mod.
  19. local EDDYFIELD_SIZE = 1 -- size of the "eddy field-lines" that smaller islands follow
  20. local ISLANDS_SEED = 1000 -- You only need to change this if you want to try different island layouts without changing the map seed
  21. -- Some lists of known node aliases (any nodes which can't be found won't be used).
  22. local NODENAMES_STONE = {"epic:stone", "mapgen_stone", "mcl_core:stone", "default:stone", "main:stone"}
  23. local NODENAMES_WATER = {"mapgen_water_source", "mcl_core:water_source", "default:water_source", "main:water"}
  24. local NODENAMES_ICE = {"mapgen_ice", "mcl_core:ice", "pedology:ice_white", "default:ice", "main:ice"}
  25. local NODENAMES_GRAVEL = {"mapgen_gravel", "mcl_core:gravel", "default:gravel", "main:gravel"}
  26. local NODENAMES_GRASS = {"mapgen_dirt_with_grass", "mcl_core:dirt_with_grass", "default:dirt_with_grass", "main:grass"} -- currently only used with games that don't register biomes, e.g. Hades Revisted
  27. local NODENAMES_DIRT = {"mapgen_dirt", "mcl_core:dirt", "default:dirt", "main:dirt"} -- currently only used with games that don't register biomes, e.g. Hades Revisted
  28. local NODENAMES_SILT = {"mapgen_silt", "default:silt", "aotearoa:silt", "darkage:silt", "mapgen_sand", "mcl_core:sand", "default:sand", "main:sand"} -- silt isn't a thing yet, but perhaps one day it will be. Use sand for the bottom of ponds in the meantime.
  29. local NODENAMES_VINES = {"mcl_core:vine", "vines:side_end", "ethereal:vine", "main:vine"} -- ethereal vines don't grow, so only select that if there's nothing else.
  30. local NODENAMES_HANGINGVINE = {"vines:vine_end"}
  31. local NODENAMES_HANGINGROOT = {"vines:root_end"}
  32. local NODENAMES_TREEWOOD = {"mcl_core:tree", "default:tree", "mapgen_tree", "main:tree"}
  33. local NODENAMES_TREELEAVES = {"mcl_core:leaves", "default:leaves", "mapgen_leaves", "main:leaves"}
  34. local NODENAMES_FRAMEGLASS = {"xpanes:obsidian_pane_flat", "xpanes:pane_flat", "default:glass", "xpanes:pane_natural_flat", "mcl_core:glass", "walls:window"}
  35. local NODENAMES_WOOD = {"default:wood", "mcl_core:wood", "main:wood"}
  36. local MODNAME = minetest.get_current_modname()
  37. local VINES_REQUIRED_HUMIDITY = 49
  38. local VINES_REQUIRED_TEMPERATURE = 40
  39. local ICE_REQUIRED_TEMPERATURE = 8
  40. local DEBUG = false -- dev logging
  41. local DEBUG_GEOMETRIC = false -- turn off noise from island shapes
  42. local DEBUG_SKYTREES = false -- dev logging
  43. -- OVERDRAW can be set to 1 to cause a y overdraw of one node above the chunk, to avoid creating a dirt "surface"
  44. -- at the top of the chunk that trees mistakenly grow on when the chunk is decorated.
  45. -- However, it looks like that tree problem has been solved by either engine or biome updates, and overdraw causes
  46. -- it's own issues (e.g. nodeId_top not getting set correctly), so I'm leaving overdraw off (i.e. zero) until I
  47. -- notice problems requiring it.
  48. local OVERDRAW = 0
  49. local S = minetest.get_translator(MODNAME)
  50. cloudlands = {} -- API functions can be accessed via this global:
  51. -- cloudlands.get_island_details(minp, maxp) -- returns an array of island-information-tables, y is ignored.
  52. -- cloudlands.find_nearest_island(x, z, search_radius) -- returns a single island-information-table, or nil
  53. -- cloudlands.get_height_at(x, z, [island-information-tables]) -- returns (y, isWater), or nil if no island here
  54. cloudlands.coreTypes = {
  55. {
  56. territorySize = 200,
  57. coresPerTerritory = 3,
  58. radiusMax = 96,
  59. depthMax = 50,
  60. thicknessMax = 8,
  61. frequency = 0.1,
  62. pondWallBuffer = 0.03,
  63. requiresNexus = true,
  64. exclusive = false
  65. },
  66. {
  67. territorySize = 60,
  68. coresPerTerritory = 1,
  69. radiusMax = 40,
  70. depthMax = 40,
  71. thicknessMax = 4,
  72. frequency = 0.1,
  73. pondWallBuffer = 0.06,
  74. requiresNexus = false,
  75. exclusive = true
  76. },
  77. {
  78. territorySize = 30,
  79. coresPerTerritory = 3,
  80. radiusMax = 16, -- I feel this and depthMax should be bigger, say 18, and territorySize increased to 34 to match, but I can't change it any more or existing worlds will mismatch along previously emerged chunk boundaries
  81. depthMax = 16,
  82. thicknessMax = 2,
  83. frequency = 0.1,
  84. pondWallBuffer = 0.11, -- larger values will make ponds smaller and further from island edges, so it should be as low as you can get it without the ponds leaking over the edge. A small leak-prone island is at (3160, -2360) on seed 1
  85. requiresNexus = false,
  86. exclusive = true
  87. }
  88. }
  89. if minetest.get_biome_data == nil then error(MODNAME .. " requires Minetest v5.0 or greater", 0) end
  90. local function fromSettings(settings_name, default_value)
  91. local result
  92. if type(default_value) == "number" then
  93. result = tonumber(minetest.settings:get(settings_name) or default_value)
  94. elseif type(default_value) == "boolean" then
  95. result = minetest.settings:get_bool(settings_name, default_value)
  96. end
  97. return result
  98. end
  99. -- override any settings with user-specified values before these values are needed
  100. ALTITUDE = fromSettings(MODNAME .. "_altitude", ALTITUDE)
  101. ALTITUDE_AMPLITUDE = fromSettings(MODNAME .. "_altitude_amplitude", ALTITUDE_AMPLITUDE)
  102. GENERATE_ORES = fromSettings(MODNAME .. "_generate_ores", GENERATE_ORES)
  103. VINE_COVERAGE = fromSettings(MODNAME .. "_vine_coverage", VINE_COVERAGE * 100) / 100
  104. LOWLAND_BIOMES = fromSettings(MODNAME .. "_use_lowland_biomes", LOWLAND_BIOMES)
  105. TREE_RARITY = fromSettings(MODNAME .. "_giant_tree_rarety", TREE_RARITY * 100) / 100
  106. BIOLUMINESCENCE = fromSettings(MODNAME .. "_bioluminescence", BIOLUMINESCENCE)
  107. ENABLE_PORTALS = fromSettings(MODNAME .. "_enable_portals", ENABLE_PORTALS)
  108. local noiseparams_eddyField = {
  109. offset = -1,
  110. scale = 2,
  111. spread = {x = 350 * EDDYFIELD_SIZE, y = 350 * EDDYFIELD_SIZE, z= 350 * EDDYFIELD_SIZE},
  112. seed = ISLANDS_SEED, --WARNING! minetest.get_perlin() will add the server map's seed to this value
  113. octaves = 2,
  114. persistence = 0.7,
  115. lacunarity = 2.0,
  116. }
  117. local noiseparams_heightMap = {
  118. offset = 0,
  119. scale = ALTITUDE_AMPLITUDE,
  120. spread = {x = 160, y = 160, z= 160},
  121. seed = ISLANDS_SEED, --WARNING! minetest.get_perlin() will add the server map's seed to this value
  122. octaves = 3,
  123. persistence = 0.5,
  124. lacunarity = 2.0,
  125. }
  126. local DENSITY_OFFSET = 0.7
  127. local noiseparams_density = {
  128. offset = DENSITY_OFFSET,
  129. scale = .3,
  130. spread = {x = 25, y = 25, z= 25},
  131. seed = 1000, --WARNING! minetest.get_perlin() will add the server map's seed to this value
  132. octaves = 4,
  133. persistence = 0.5,
  134. lacunarity = 2.0,
  135. }
  136. local SURFACEMAP_OFFSET = 0.5
  137. local noiseparams_surfaceMap = {
  138. offset = SURFACEMAP_OFFSET,
  139. scale = .5,
  140. spread = {x = 40, y = 40, z= 40},
  141. seed = ISLANDS_SEED, --WARNING! minetest.get_perlin() will add the server map's seed to this value
  142. octaves = 4,
  143. persistence = 0.5,
  144. lacunarity = 2.0,
  145. }
  146. local noiseparams_skyReef = {
  147. offset = .3,
  148. scale = .9,
  149. spread = {x = 3, y = 3, z= 3},
  150. seed = 1000,
  151. octaves = 2,
  152. persistence = 0.5,
  153. lacunarity = 2.0,
  154. }
  155. local noiseAngle = -15 --degrees to rotate eddyField noise, so that the vertical and horizontal tendencies are off-axis
  156. local ROTATE_COS = math.cos(math.rad(noiseAngle))
  157. local ROTATE_SIN = math.sin(math.rad(noiseAngle))
  158. local noise_eddyField
  159. local noise_heightMap
  160. local noise_density
  161. local noise_surfaceMap
  162. local noise_skyReef
  163. local worldSeed
  164. local nodeId_ignore = minetest.CONTENT_IGNORE
  165. local nodeId_air
  166. local nodeId_stone
  167. local nodeId_grass
  168. local nodeId_dirt
  169. local nodeId_water
  170. local nodeId_ice
  171. local nodeId_silt
  172. local nodeId_gravel
  173. local nodeId_vine
  174. local nodeName_vine
  175. local nodeName_ignore = minetest.get_name_from_content_id(nodeId_ignore)
  176. local REQUIRED_DENSITY = 0.4
  177. local randomNumbers = {} -- array of 0-255 random numbers with values between 0 and 1 (inclusive)
  178. local data = {} -- reuse the massive VoxelManip memory buffers instead of creating on every on_generate()
  179. local biomes = {}
  180. -- optional region specified in settings to restrict islands too
  181. local region_restrictions = false
  182. local region_min_x, region_min_z, region_max_x, region_max_z = -32000, -32000, 32000, 32000
  183. -- optional biomes specified in settings to restrict islands too
  184. local limit_to_biomes = nil
  185. local limit_to_biomes_altitude = nil
  186. --[[==============================
  187. Math functions
  188. ==============================]]--
  189. -- avoid having to perform table lookups each time a common math function is invoked
  190. local math_min, math_max, math_floor, math_sqrt, math_cos, math_sin, math_abs, math_pow, PI = math.min, math.max, math.floor, math.sqrt, math.cos, math.sin, math.abs, math.pow, math.pi
  191. local function clip(value, minValue, maxValue)
  192. if value <= minValue then
  193. return minValue
  194. elseif value >= maxValue then
  195. return maxValue
  196. else
  197. return value
  198. end
  199. end
  200. local function round(value)
  201. return math_floor(0.5 + value)
  202. end
  203. --[[==============================
  204. Interop functions
  205. ==============================]]--
  206. local get_heat, get_humidity = minetest.get_heat, minetest.get_humidity
  207. local biomeinfoAvailable = minetest.get_modpath("biomeinfo") ~= nil and minetest.global_exists("biomeinfo")
  208. local isMapgenV6 = minetest.get_mapgen_setting("mg_name") == "v6"
  209. if isMapgenV6 then
  210. if not biomeinfoAvailable then
  211. -- The biomeinfo mod by Wuzzy can be found at https://repo.or.cz/minetest_biomeinfo.git
  212. minetest.log("warning", MODNAME .. " detected mapgen v6: Full mapgen v6 support requires adding the biomeinfo mod.")
  213. else
  214. get_heat = function(pos)
  215. return biomeinfo.get_v6_heat(pos) * 100
  216. end
  217. get_humidity = function(pos)
  218. return biomeinfo.get_v6_humidity(pos) * 100
  219. end
  220. end
  221. end
  222. local interop = {}
  223. -- returns the id of the first nodename in the list that resolves to a node id, or nodeId_ignore if not found
  224. interop.find_node_id = function (node_contender_names)
  225. local result = nodeId_ignore
  226. for _,contenderName in ipairs(node_contender_names) do
  227. local nonAliasName = minetest.registered_aliases[contenderName] or contenderName
  228. if minetest.registered_nodes[nonAliasName] ~= nil then
  229. result = minetest.get_content_id(nonAliasName)
  230. end
  231. --if DEBUG then minetest.log("info", contenderName .. " returned " .. result .. " (" .. minetest.get_name_from_content_id(result) .. ")") end
  232. if result ~= nodeId_ignore then return result end
  233. end
  234. return result
  235. end
  236. -- returns the name of the first nodename in the list that resolves to a node id, or 'ignore' if not found
  237. interop.find_node_name = function (node_contender_names)
  238. return minetest.get_name_from_content_id(interop.find_node_id(node_contender_names))
  239. end
  240. interop.get_first_element_in_table = function(tbl)
  241. for k,v in pairs(tbl) do return v end
  242. return nil
  243. end
  244. -- returns the top-texture name of the first nodename in the list that's a registered node, or nil if not found
  245. interop.find_node_texture = function (node_contender_names)
  246. local result = nil
  247. local nodeName = minetest.get_name_from_content_id(interop.find_node_id(node_contender_names))
  248. if nodeName ~= nil then
  249. local node = minetest.registered_nodes[nodeName]
  250. if node ~= nil then
  251. result = node.tiles
  252. if type(result) == "table" then result = result["name"] or interop.get_first_element_in_table(result) end -- incase it's not a string
  253. if type(result) == "table" then result = result["name"] or interop.get_first_element_in_table(result) end -- incase multiple tile definitions
  254. end
  255. end
  256. return result
  257. end
  258. -- returns the node name of the clone node.
  259. interop.register_clone = function(node_name, clone_name)
  260. local node = minetest.registered_nodes[node_name]
  261. if node == nil then
  262. minetest.log("error", "cannot clone " .. node_name)
  263. return nil
  264. else
  265. if clone_name == nil then clone_name = MODNAME .. ":" .. string.gsub(node.name, ":", "_") end
  266. if minetest.registered_nodes[clone_name] == nil then
  267. if DEBUG then minetest.log("info", "attempting to register: " .. clone_name) end
  268. local clone = {}
  269. for key, value in pairs(node) do clone[key] = value end
  270. clone.name = clone_name
  271. minetest.register_node(clone_name, clone)
  272. --minetest.log("info", clone_name .. " id: " .. minetest.get_content_id(clone_name))
  273. --minetest.log("info", clone_name .. ": " .. dump(minetest.registered_nodes[clone_name]))
  274. end
  275. return clone_name
  276. end
  277. end
  278. -- converts "modname:nodename" into (modname, nodename), if no colon is found then modname is nil
  279. interop.split_nodename = function(nodeName)
  280. local result_modname = nil
  281. local result_nodename = nodeName
  282. local pos = nodeName:find(':')
  283. if pos ~= nil then
  284. result_modname = nodeName:sub(0, pos - 1)
  285. result_nodename = nodeName:sub(pos + 1)
  286. end
  287. return result_modname, result_nodename
  288. end;
  289. -- returns a unique id for the biome, normally this is numeric but with mapgen v6 it can be a string name.
  290. interop.get_biome_key = function(pos)
  291. if isMapgenV6 and biomeinfoAvailable then
  292. return biomeinfo.get_v6_biome(pos)
  293. else
  294. return minetest.get_biome_data(pos).biome
  295. end
  296. end
  297. -- returns true if filename is a file that exists.
  298. interop.file_exists = function(filename)
  299. local f = io.open(filename, "r")
  300. if f == nil then
  301. return false
  302. else
  303. f:close()
  304. return true
  305. end
  306. end
  307. -- returns a written book item (technically an item stack), or nil if no books mod available
  308. interop.write_book = function(title, author, text, description)
  309. local stackName_writtenBook
  310. if minetest.get_modpath("mcl_books") then
  311. stackName_writtenBook = "mcl_books:written_book"
  312. text = title .. "\n\n" .. text -- MineClone2 books doen't show a title (or author)
  313. elseif minetest.get_modpath("book") ~= nil then
  314. stackName_writtenBook = "book:book_written"
  315. text = "\n\n" .. text -- Crafter books put the text immediately under the title
  316. elseif minetest.get_modpath("default") ~= nil then
  317. stackName_writtenBook = "default:book_written"
  318. else
  319. return nil
  320. end
  321. local book_itemstack = ItemStack(stackName_writtenBook)
  322. local book_data = {}
  323. book_data.title = title
  324. book_data.text = text
  325. book_data.owner = author
  326. book_data.author = author
  327. book_data.description = description
  328. book_data.page = 1
  329. book_data.page_max = 1
  330. book_data.generation = 0
  331. book_data["book.book_title"] = title -- Crafter book title
  332. book_data["book.book_text"] = text -- Crafter book text
  333. book_itemstack:get_meta():from_table({fields = book_data})
  334. return book_itemstack
  335. end
  336. --[[==============================
  337. Portals
  338. ==============================]]--
  339. local addDetail_ancientPortal = nil;
  340. if ENABLE_PORTALS and minetest.get_modpath("nether") ~= nil and minetest.global_exists("nether") and nether.register_portal ~= nil then
  341. -- The Portals API is available
  342. -- Register a player-buildable portal to Hallelujah Mountains.
  343. -- returns a position on the island which is suitable for a portal to be placed, or nil if none can be found
  344. local function find_potential_portal_location_on_island(island_info)
  345. local result = nil
  346. if island_info ~= nil then
  347. local searchRadius = island_info.radius * 0.6 -- islands normally don't reach their full radius, and lets not put portals too near the edge
  348. local coreList = cloudlands.get_island_details(
  349. {x = island_info.x - searchRadius, z = island_info.z - searchRadius},
  350. {x = island_info.x + searchRadius, z = island_info.z + searchRadius}
  351. );
  352. -- Deterministically sample the island for a low location that isn't water.
  353. -- Seed the prng so this function always returns the same coords for the island
  354. local prng = PcgRandom(island_info.x * 65732 + island_info.z * 729 + minetest.get_mapgen_setting("seed") * 3)
  355. local positions = {}
  356. for attempt = 1, 15 do -- how many attempts we'll make at finding a good location
  357. local angle = (prng:next(0, 10000) / 10000) * 2 * PI
  358. local distance = math_sqrt(prng:next(0, 10000) / 10000) * searchRadius
  359. if attempt == 1 then distance = 0 end -- Always sample the middle of the island, as it's the safest fallback location
  360. local x = round(island_info.x + math_cos(angle) * distance)
  361. local z = round(island_info.z + math_sin(angle) * distance)
  362. local y, isWater = cloudlands.get_height_at(x, z, coreList)
  363. if y ~= nil then
  364. local weight = 0
  365. if not isWater then weight = weight + 1 end -- avoid putting portals in ponds
  366. if y >= island_info.y + ALTITUDE then weight = weight + 2 end -- avoid putting portals down the sides of eroded cliffs
  367. positions[#positions + 1] = {x = x, y = y + 1, z = z, weight = weight}
  368. end
  369. end
  370. -- Order the locations by how good they are
  371. local compareFn = function(pos_a, pos_b)
  372. if pos_a.weight > pos_b.weight then return true end
  373. if pos_a.weight == pos_b.weight and pos_a.y < pos_b.y then return true end -- I can't justify why I think lower positions are better. I'm imagining portals nested in valleys rather than on ridges.
  374. return false
  375. end
  376. table.sort(positions, compareFn)
  377. -- Now the locations are sorted by how good they are, find the first/best that doesn't
  378. -- grief a player build.
  379. -- Ancient Portalstone has is_ground_content set to true, so we won't have to worry about
  380. -- old/broken portal frames interfering with the results of nether.volume_is_natural()
  381. for _, position in ipairs(positions) do
  382. -- Unfortunately, at this point we don't know the orientation of the portal, so use worst case
  383. local minp = {x = position.x - 2, y = position.y, z = position.z - 2}
  384. local maxp = {x = position.x + 3, y = position.y + 4, z = position.z + 3}
  385. if nether.volume_is_natural(minp, maxp) then
  386. result = position
  387. break
  388. end
  389. end
  390. end
  391. return result
  392. end
  393. -- returns nil if no suitable location could be found, otherwise returns (portal_pos, island_info)
  394. local function find_nearest_island_location_for_portal(surface_x, surface_z)
  395. local result = nil
  396. local island = cloudlands.find_nearest_island(surface_x, surface_z, 75)
  397. if island == nil then island = cloudlands.find_nearest_island(surface_x, surface_z, 150) end
  398. if island == nil then island = cloudlands.find_nearest_island(surface_x, surface_z, 400) end
  399. if island ~= nil then
  400. result = find_potential_portal_location_on_island(island)
  401. end
  402. return result, island
  403. end
  404. -- Ideally the Nether mod will provide a block obtainable by exploring the Nether which is
  405. -- earmarked for mods like this one to use for portals, but until this happens I'll create
  406. -- our own tempory placeholder "portalstone".
  407. -- The Portals API is currently provided by nether, which depends on default, so we can assume default textures are available
  408. local portalstone_end = "default_furnace_top.png^(default_ice.png^[opacity:120)^[multiply:#668" -- this gonna look bad with non-default texturepacks, hopefully Nether mod will provide a real block
  409. local portalstone_side = "[combine:16x16:0,0=default_furnace_top.png:4,0=default_furnace_top.png:8,0=default_furnace_top.png:12,0=default_furnace_top.png:^(default_ice.png^[opacity:120)^[multiply:#668"
  410. minetest.register_node("cloudlands:ancient_portalstone", {
  411. description = S("Ancient Portalstone"),
  412. tiles = {portalstone_end, portalstone_end, portalstone_side, portalstone_side, portalstone_side, portalstone_side},
  413. paramtype2 = "facedir",
  414. sounds = default.node_sound_stone_defaults(),
  415. groups = {cracky = 1, level = 2},
  416. on_blast = function() --[[blast proof]] end
  417. })
  418. minetest.register_ore({
  419. ore_type = "scatter",
  420. ore = "cloudlands:ancient_portalstone",
  421. wherein = "nether:rack",
  422. clust_scarcity = 28 * 28 * 28,
  423. clust_num_ores = 6,
  424. clust_size = 3,
  425. y_max = nether.DEPTH,
  426. y_min = nether.DEPTH_FLOOR or -32000,
  427. })
  428. local _ = {name = "air", prob = 0}
  429. local A = {name = "air", prob = 255, force_place = true}
  430. local PU = {name = "cloudlands:ancient_portalstone", param2 = 0, prob = 255, force_place = true}
  431. local PW = {name = "cloudlands:ancient_portalstone", param2 = 12, prob = 255, force_place = true}
  432. local PN = {name = "cloudlands:ancient_portalstone", param2 = 4, prob = 255, force_place = true}
  433. minetest.register_decoration({
  434. name = "Ancient broken portal",
  435. deco_type = "schematic",
  436. place_on = "nether:rack",
  437. sidelen = 80,
  438. fill_ratio = 0.0003,
  439. biomes = {"nether_caverns"},
  440. y_max = nether.DEPTH,
  441. y_min = nether.DEPTH_FLOOR or -32000,
  442. schematic = {
  443. size = {x = 4, y = 4, z = 1},
  444. data = {
  445. PN, A, PW, PN,
  446. PU, A, A, PU,
  447. A, _, _, PU,
  448. _, _, _, PU
  449. },
  450. yslice_prob = {
  451. {ypos = 3, prob = 92},
  452. {ypos = 1, prob = 30},
  453. }
  454. },
  455. place_offset_y = 1,
  456. flags = "force_placement,all_floors",
  457. rotation = "random"
  458. })
  459. -- this uses place_schematic() without minetest.after(), so should be called after vm:write_to_map()
  460. addDetail_ancientPortal = function(core)
  461. if (core.radius < 8 or PORTAL_RARITY == 0) then return false end -- avoid portals hanging off the side of small islands
  462. local fastHash = 3
  463. fastHash = (37 * fastHash) + 9354 -- to keep this probability distinct from reefs and atols
  464. fastHash = (37 * fastHash) + ISLANDS_SEED
  465. fastHash = (37 * fastHash) + core.x
  466. fastHash = (37 * fastHash) + core.z
  467. fastHash = (37 * fastHash) + math_floor(core.radius)
  468. fastHash = (37 * fastHash) + math_floor(core.depth)
  469. if (PORTAL_RARITY * 10000) < math_floor((math_abs(fastHash)) % 10000) then return false end
  470. local portalPos = find_potential_portal_location_on_island(core)
  471. if portalPos ~= nil then
  472. local orientation = (fastHash % 2) * 90
  473. portalPos.y = portalPos.y - ((core.x + core.z) % 3) -- partially bury some ancient portals
  474. minetest.place_schematic(
  475. portalPos,
  476. {
  477. size = {x = 4, y = 5, z = 1},
  478. data = {
  479. PN, PW, PW, PN,
  480. PU, _, _, PU,
  481. PU, _, _, PU,
  482. PU, _, _, PU,
  483. PN, PW, PW, PN
  484. },
  485. },
  486. orientation,
  487. { -- node replacements
  488. ["default:obsidian"] = "cloudlands:ancient_portalstone",
  489. },
  490. true
  491. )
  492. end
  493. end
  494. nether.register_portal("cloudlands_portal", {
  495. shape = nether.PortalShape_Traditional,
  496. frame_node_name = "cloudlands:ancient_portalstone",
  497. wormhole_node_color = 2, -- 2 is blue
  498. particle_color = "#77F",
  499. particle_texture = {
  500. name = "nether_particle_anim1.png",
  501. animation = {
  502. type = "vertical_frames",
  503. aspect_w = 7,
  504. aspect_h = 7,
  505. length = 1,
  506. },
  507. scale = 1.5
  508. },
  509. title = S("Hallelujah Mountains Portal"),
  510. book_of_portals_pagetext =
  511. S("Construction requires 14 blocks of ancient portalstone. We have no knowledge of how portalstones were created, the means to craft them are likely lost to time, so our only source has been to scavenge the Nether for the remnants of ancient broken portals. A finished frame is four blocks wide, five blocks high, and stands vertically, like a doorway.") .. "\n\n" ..
  512. S("The only portal we managed to scavenge enough portalstone to build took us to a land of floating islands. There were hills and forests and even water up there, but the edges are a perilous drop — a depth of which we cannot even begin to plumb."),
  513. is_within_realm = function(pos)
  514. cloudlands.init()
  515. -- return true if pos is in the cloudlands
  516. -- I'm doing this based off height for speed, so it sometimes gets it wrong when the
  517. -- Hallelujah mountains start reaching the ground.
  518. local largestCoreType = cloudlands.coreTypes[1] -- the first island type is the biggest/thickest
  519. --local island_bottom = ALTITUDE - (largestCoreType.depthMax * 0.66) + round(noise_heightMap:get_2d({x = pos.x, y = pos.z}))
  520. local island_bottom = ALTITUDE - (largestCoreType.depthMax * 0.66) + round(noise_heightMap:get_2d({x = pos.x, y = pos.z}))
  521. return pos.y > math_max(40, island_bottom)
  522. end,
  523. find_realm_anchorPos = function(surface_anchorPos)
  524. -- Find the nearest island and obtain a suitable surface position on it
  525. local destination_pos, island = find_nearest_island_location_for_portal(surface_anchorPos.x, surface_anchorPos.z)
  526. if island ~= nil then
  527. -- Allow any existing or player-positioned portal on the island to be linked to
  528. -- first before resorting to the island's default portal position
  529. local existing_portal_location, existing_portal_orientation = nether.find_nearest_working_portal(
  530. "cloudlands_portal",
  531. {x = island.x, y = 100000, z = island.z}, -- Using 100000 for y to ensure the position is in the cloudlands realm and so find_nearest_working_portal() will only returns island portals.
  532. island.radius * 0.9, -- Islands normally don't reach their full radius. Ensure this distance limit encompasses any location find_nearest_island_location_for_portal() can return.
  533. 0 -- a y_factor of 0 makes the search ignore the altitude of the portals (as long as they are in the Cloudlands realm)
  534. )
  535. if existing_portal_location ~= nil then
  536. return existing_portal_location, existing_portal_orientation
  537. end
  538. end
  539. return destination_pos
  540. end,
  541. find_surface_anchorPos = function(realm_anchorPos)
  542. -- This function isn't needed since find_surface_target_y() will be used by default,
  543. -- but by implementing it I can look for any existing nearby portals before falling
  544. -- back to find_surface_target_y.
  545. -- Using -100000 for y to ensure the position is outside the cloudlands realm and so
  546. -- find_nearest_working_portal() will only returns ground portals.
  547. -- a y_factor of 0 makes the search ignore the -100000 altitude of the portals (as
  548. -- long as they are outside the cloudlands realm)
  549. local existing_portal_location, existing_portal_orientation =
  550. nether.find_nearest_working_portal("cloudlands_portal", {x = realm_anchorPos.x, y = -100000, z = realm_anchorPos.z}, 150, 0)
  551. if existing_portal_location ~= nil then
  552. return existing_portal_location, existing_portal_orientation
  553. else
  554. local y = nether.find_surface_target_y(realm_anchorPos.x, realm_anchorPos.z, "cloudlands_portal")
  555. return {x = realm_anchorPos.x, y = y, z = realm_anchorPos.z}
  556. end
  557. end,
  558. on_ignite = function(portalDef, anchorPos, orientation)
  559. -- make some sparks fly on ignition
  560. local p1, p2 = portalDef.shape:get_p1_and_p2_from_anchorPos(anchorPos, orientation)
  561. local pos = vector.divide(vector.add(p1, p2), 2)
  562. local textureName = portalDef.particle_texture
  563. if type(textureName) == "table" then textureName = textureName.name end
  564. local velocity
  565. if orientation == 0 then
  566. velocity = {x = 0, y = 0, z = 7}
  567. else
  568. velocity = {x = 7, y = 0, z = 0}
  569. end
  570. local particleSpawnerDef = {
  571. amount = 180,
  572. time = 0.15,
  573. minpos = {x = pos.x - 1, y = pos.y - 1.5, z = pos.z - 1},
  574. maxpos = {x = pos.x + 1, y = pos.y + 1.5, z = pos.z + 1},
  575. minvel = velocity,
  576. maxvel = velocity,
  577. minacc = {x = 0, y = 0, z = 0},
  578. maxacc = {x = 0, y = 0, z = 0},
  579. minexptime = 0.1,
  580. maxexptime = 0.5,
  581. minsize = 0.3 * portalDef.particle_texture_scale,
  582. maxsize = 0.8 * portalDef.particle_texture_scale,
  583. collisiondetection = false,
  584. texture = textureName .. "^[colorize:#99F:alpha",
  585. animation = portalDef.particle_texture_animation,
  586. glow = 8
  587. }
  588. minetest.add_particlespawner(particleSpawnerDef)
  589. velocity = vector.multiply(velocity, -1);
  590. particleSpawnerDef.minvel, particleSpawnerDef.maxvel = velocity, velocity
  591. minetest.add_particlespawner(particleSpawnerDef)
  592. end
  593. })
  594. end
  595. --[[==============================
  596. SkyTrees
  597. ==============================]]--
  598. -- If splitting SkyTrees into a seperate mod, perhaps schemlib would be of help - https://forum.minetest.net/viewtopic.php?t=18084
  599. if not minetest.global_exists("SkyTrees") then -- If SkyTrees added into other mods, this may have already been defined
  600. local TREE1_FILE = 'cloudlands_tree1.mts'
  601. local TREE2_FILE = 'cloudlands_tree2.mts'
  602. local BARK_SUFFIX = '_bark'
  603. local GLOW_SUFFIX = '_glow'
  604. SkyTrees = {
  605. -- Order the trees in this schematicInfo array from the largest island requirements to smallest
  606. -- The data in each schematicInfo must exactly match what's in the .mts file or things will break
  607. schematicInfo = {
  608. {
  609. filename = TREE1_FILE,
  610. size = {x = 81, y = 106, z = 111},
  611. center = {x = 37, y = 11, z = 73},
  612. requiredIslandDepth = 20,
  613. requiredIslandRadius = 40,
  614. nodesWithConstructor = {
  615. {x=35, y=69, z=1}, {x=61, y=51, z=2}, {x=36, y=68, z=2}, {x=68, y=48, z=3}, {x=61, y=50, z=4}, {x=71, y=50, z=5}, {x=58, y=52, z=5}, {x=65, y=50, z=9}, {x=72, y=53, z=11}, {x=41, y=67, z=12}, {x=63, y=48, z=13}, {x=69, y=52, z=13}, {x=33, y=66, z=14}, {x=39, y=68, z=15}, {x=72, y=68, z=15}, {x=40, y=67, z=16}, {x=39, y=66, z=17}, {x=68, y=45, z=19}, {x=69, y=44, z=20}, {x=72, y=55, z=20}, {x=66, y=56, z=20}, {x=58, y=66, z=20}, {x=71, y=58, z=21}, {x=68, y=45, z=22}, {x=70, y=51, z=22}, {x=73, y=55, z=22}, {x=36, y=62, z=22}, {x=70, y=67, z=22}, {x=21, y=65, z=23}, {x=22, y=66, z=23}, {x=53, y=66, z=23}, {x=70, y=68, z=23}, {x=73, y=54, z=24}, {x=75, y=57, z=24}, {x=37, y=63, z=24}, {x=7, y=68, z=24}, {x=69, y=56, z=25}, {x=34, y=58, z=25}, {x=66, y=62, z=25}, {x=64, y=66, z=25}, {x=6, y=67, z=25}, {x=3, y=68, z=25}, {x=68, y=56, z=26}, {x=65, y=57, z=26}, {x=61, y=63, z=26}, {x=31, y=59, z=27}, {x=48, y=62, z=27}, {x=50, y=63, z=27}, {x=78, y=65, z=27}, {x=78, y=52, z=28}, {x=68, y=57, z=28}, {x=76, y=57, z=28}, {x=31, y=60, z=28}, {x=15, y=63, z=28}, {x=16, y=63, z=28}, {x=66, y=64, z=28}, {x=60, y=65, z=28}, {x=61, y=76, z=28}, {x=63, y=76, z=28}, {x=69, y=59, z=29}, {x=51, y=65, z=29}, {x=72, y=57, z=30}, {x=20, y=60, z=30}, {x=21, y=61, z=30}, {x=49, y=65, z=30}, {x=52, y=53, z=31}, {x=72, y=57, z=31}, {x=36, y=58, z=31}, {x=63, y=60, z=31}, {x=54, y=63, z=31}, {x=45, y=65, z=31}, {x=79, y=66, z=31}, {x=62, y=70, z=31}, {x=55, y=103, z=31}, {x=52, y=53, z=32}, {x=68, y=60, z=32}, {x=19, y=61, z=32}, {x=53, y=63, z=32}, {x=37, y=64, z=32}, {x=21, y=65, z=32}, {x=56, y=65, z=32}, {x=59, y=71, z=32}, {x=35, y=74, z=32}, {x=23, y=75, z=32}, {x=35, y=58, z=33}, {x=62, y=60, z=33}, {x=18, y=63, z=33}, {x=73, y=67, z=33}, {x=37, y=74, z=33}, {x=65, y=75, z=33}, {x=38, y=2, z=34}, {x=67, y=52, z=34}, {x=71, y=60, z=34}, {x=25, y=63, z=34}, {x=19, y=64, z=34}, {x=32, y=66, z=34}, {x=66, y=72, z=34}, {x=41, y=81, z=34}, {x=45, y=93, z=34}, {x=54, y=99, z=34}, {x=38, y=5, z=35}, {x=68, y=48, z=35}, {x=69, y=51, z=35}, {x=48, y=53, z=35}, {x=37, y=57, z=35}, {x=77, y=58, z=35}, {x=32, y=60, z=35}, {x=20, y=61, z=35}, {x=27, y=61, z=35}, {x=33, y=65, z=35}, {x=58, y=65, z=35}, {x=58, y=72, z=35}, {x=60, y=73, z=35}, {x=30, y=74, z=35}, {x=41, y=74, z=35}, {x=41, y=87, z=35}, {x=22, y=58, z=36}, {x=64, y=58, z=36}, {x=39, y=70, z=36}, {x=36, y=77, z=36}, {x=44, y=83, z=36}, {x=40, y=86, z=36}, {x=35, y=56, z=37}, {x=65, y=59, z=37}, {x=66, y=62, z=37}, {x=62, y=67, z=37}, {x=39, y=68, z=37}, {x=40, y=86, z=37}, {x=53, y=88, z=37}, {x=43, y=97, z=37}, {x=52, y=99, z=37}, {x=37, y=3, z=38}, {x=35, y=55, z=38}, {x=38, y=56, z=38}, {x=25, y=57, z=38}, {x=65, y=57, z=38}, {x=71, y=61, z=38}, {x=33, y=65, z=38}, {x=61, y=65, z=38}, {x=50, y=66, z=38}, {x=38, y=68, z=38}, {x=46, y=97, z=38}, {x=44, y=100, z=38}, {x=51, y=102, z=38}, {x=29, y=42, z=39}, {x=27, y=43, z=39}, {x=70, y=48, z=39}, {x=72, y=52, z=39}, {x=23, y=57, z=39}, {x=26, y=57, z=39}, {x=28, y=58, z=39}, {x=55, y=58, z=39}, {x=73, y=59, z=39}, {x=65, y=65, z=39}, {x=41, y=68, z=39}, {x=42, y=81, z=39}, {x=55, y=88, z=39}, {x=43, y=91, z=39}, {x=45, y=100, z=39}, {x=23, y=57, z=40}, {x=29, y=57, z=40}, {x=76, y=58, z=40}, {x=73, y=59, z=40}, {x=78, y=59, z=40}, {x=31, y=60, z=40}, {x=64, y=64, z=40}, {x=41, y=67, z=40}, {x=42, y=75, z=40}, {x=37, y=78, z=40}, {x=42, y=92, z=40}, {x=51, y=101, z=40}, {x=48, y=105, z=40}, {x=75, y=59, z=41}, {x=55, y=63, z=41}, {x=35, y=68, z=41}, {x=35, y=69, z=41}, {x=35, y=71, z=41}, {x=34, y=42, z=42}, {x=29, y=55, z=42}, {x=50, y=61, z=42}, {x=34, y=65, z=42}, {x=57, y=88, z=42}, {x=48, y=89, z=42}, {x=49, y=89, z=42}, {x=27, y=22, z=43}, {x=26, y=28, z=43}, {x=31, y=46, z=43}, {x=66, y=52, z=43}, {x=49, y=57, z=43}, {x=56, y=57, z=43}, {x=41, y=69, z=43}, {x=36, y=52, z=44}, {x=63, y=54, z=44}, {x=51, y=55, z=44}, {x=57, y=56, z=44}, {x=69, y=57, z=44}, {x=64, y=65, z=44}, {x=55, y=90, z=44}, {x=30, y=42, z=45}, {x=31, y=52, z=45}, {x=51, y=54, z=45}, {x=24, y=57, z=45}, {x=70, y=62, z=45}, {x=39, y=69, z=45}, {x=35, y=80, z=45}, {x=29, y=81, z=45}, {x=44, y=85, z=45}, {x=41, y=86, z=45}, {x=33, y=9, z=46}, {x=28, y=44, z=46}, {x=50, y=54, z=46}, {x=47, y=55, z=46}, {x=45, y=56, z=46}, {x=45, y=58, z=46}, {x=47, y=58, z=46}, {x=30, y=63, z=46}, {x=27, y=81, z=46}, {x=28, y=81, z=46}, {x=40, y=86, z=46}, {x=29, y=16, z=47}, {x=32, y=10, z=48}, {x=66, y=49, z=48}, {x=29, y=52, z=48}, {x=53, y=54, z=48}, {x=55, y=54, z=48}, {x=61, y=58, z=48}, {x=59, y=61, z=48}, {x=50, y=63, z=48}, {x=26, y=82, z=48}, {x=43, y=85, z=48}, {x=48, y=86, z=48}, {x=31, y=19, z=49}, {x=30, y=46, z=49}, {x=63, y=51, z=49}, {x=41, y=53, z=49}, {x=31, y=60, z=49}, {x=67, y=1, z=50}, {x=37, y=8, z=50}, {x=40, y=30, z=50}, {x=43, y=57, z=50}, {x=59, y=57, z=50}, {x=60, y=57, z=50}, {x=29, y=61, z=50}, {x=34, y=63, z=50}, {x=49, y=65, z=50}, {x=65, y=3, z=51}, {x=45, y=29, z=51}, {x=41, y=58, z=51}, {x=42, y=60, z=51}, {x=46, y=64, z=51}, {x=47, y=67, z=51}, {x=52, y=68, z=51}, {x=69, y=51, z=52}, {x=53, y=55, z=52}, {x=45, y=62, z=52}, {x=64, y=2, z=53}, {x=3, y=3, z=53}, {x=10, y=6, z=53}, {x=31, y=14, z=53}, {x=37, y=35, z=53}, {x=43, y=48, z=53}, {x=71, y=50, z=53}, {x=52, y=54, z=53}, {x=43, y=57, z=53}, {x=55, y=57, z=53}, {x=52, y=67, z=53}, {x=48, y=72, z=53}, {x=5, y=1, z=54}, {x=9, y=4, z=54}, {x=62, y=4, z=54}, {x=33, y=8, z=54}, {x=42, y=29, z=54}, {x=42, y=32, z=54}, {x=43, y=34, z=54}, {x=41, y=39, z=54}, {x=41, y=57, z=54}, {x=34, y=61, z=54}, {x=58, y=2, z=55}, {x=59, y=3, z=55}, {x=38, y=7, z=55}, {x=40, y=12, z=55}, {x=38, y=39, z=55}, {x=33, y=46, z=55}, {x=28, y=54, z=55}, {x=29, y=55, z=55}, {x=30, y=57, z=55}, {x=54, y=58, z=55}, {x=52, y=63, z=55}, {x=37, y=7, z=56}, {x=55, y=8, z=56}, {x=33, y=45, z=56}, {x=58, y=0, z=57}, {x=9, y=5, z=57}, {x=34, y=7, z=57}, {x=54, y=8, z=57}, {x=17, y=9, z=57}, {x=32, y=12, z=57}, {x=37, y=39, z=57}, {x=41, y=45, z=57}, {x=31, y=46, z=57}, {x=49, y=50, z=57}, {x=50, y=56, z=57}, {x=46, y=59, z=57}, {x=48, y=66, z=57}, {x=51, y=67, z=57}, {x=15, y=3, z=58}, {x=8, y=10, z=58}, {x=41, y=11, z=58}, {x=40, y=13, z=58}, {x=42, y=45, z=58}, {x=50, y=51, z=58}, {x=20, y=5, z=59}, {x=19, y=7, z=59}, {x=22, y=8, z=59}, {x=23, y=9, z=59}, {x=40, y=13, z=59}, {x=33, y=14, z=59}, {x=42, y=41, z=59}, {x=20, y=6, z=60}, {x=9, y=8, z=60}, {x=46, y=8, z=60}, {x=34, y=39, z=60}, {x=30, y=52, z=60}, {x=43, y=57, z=60}, {x=18, y=5, z=61}, {x=11, y=10, z=61}, {x=36, y=36, z=61}, {x=47, y=55, z=61}, {x=38, y=56, z=61}, {x=61, y=59, z=61}, {x=56, y=60, z=61}, {x=36, y=6, z=62}, {x=55, y=7, z=62}, {x=26, y=10, z=62}, {x=29, y=13, z=62}, {x=46, y=13, z=62}, {x=57, y=60, z=62}, {x=18, y=7, z=63}, {x=30, y=11, z=63}, {x=53, y=13, z=63}, {x=45, y=14, z=63}, {x=36, y=32, z=63}, {x=46, y=41, z=63}, {x=29, y=43, z=63}, {x=29, y=44, z=63}, {x=29, y=46, z=63}, {x=29, y=50, z=63}, {x=30, y=52, z=63}, {x=46, y=54, z=63}, {x=19, y=6, z=64}, {x=54, y=8, z=64}, {x=16, y=11, z=64}, {x=42, y=16, z=64}, {x=36, y=25, z=64}, {x=37, y=27, z=64}, {x=36, y=28, z=64}, {x=37, y=29, z=64}, {x=40, y=33, z=64}, {x=30, y=36, z=64}, {x=43, y=39, z=64}, {x=62, y=61, z=64}, {x=21, y=6, z=65}, {x=24, y=6, z=65}, {x=53, y=10, z=65}, {x=52, y=12, z=65}, {x=27, y=17, z=65}, {x=39, y=17, z=65}, {x=29, y=19, z=65}, {x=32, y=22, z=65}, {x=28, y=42, z=65}, {x=60, y=61, z=65}, {x=24, y=6, z=66}, {x=26, y=6, z=66}, {x=19, y=12, z=66}, {x=28, y=20, z=66}, {x=31, y=26, z=66}, {x=39, y=55, z=66}, {x=42, y=6, z=67}, {x=24, y=7, z=67}, {x=20, y=14, z=67}, {x=41, y=21, z=67}, {x=28, y=22, z=67}, {x=29, y=46, z=67},
  616. {x=34, y=52, z=67}, {x=45, y=17, z=68}, {x=42, y=25, z=68}, {x=28, y=43, z=68}, {x=46, y=44, z=68}, {x=29, y=7, z=69}, {x=49, y=12, z=69}, {x=29, y=43, z=69}, {x=48, y=9, z=70}, {x=45, y=17, z=70}, {x=36, y=9, z=71}, {x=47, y=10, z=71}, {x=25, y=11, z=71}, {x=45, y=17, z=71}, {x=42, y=46, z=71}, {x=34, y=47, z=71}, {x=35, y=48, z=71}, {x=45, y=10, z=72}, {x=25, y=12, z=72}, {x=45, y=35, z=72}, {x=45, y=43, z=72}, {x=36, y=52, z=72}, {x=39, y=55, z=72}, {x=26, y=19, z=73}, {x=27, y=21, z=73}, {x=26, y=27, z=73}, {x=26, y=29, z=73}, {x=43, y=31, z=73}, {x=28, y=36, z=73}, {x=42, y=41, z=73}, {x=34, y=46, z=73}, {x=39, y=59, z=73}, {x=24, y=9, z=74}, {x=48, y=9, z=74}, {x=35, y=48, z=74}, {x=35, y=51, z=74}, {x=42, y=53, z=74}, {x=33, y=57, z=74}, {x=30, y=60, z=74}, {x=47, y=8, z=75}, {x=22, y=12, z=75}, {x=45, y=18, z=75}, {x=27, y=30, z=75}, {x=45, y=33, z=75}, {x=36, y=49, z=75}, {x=36, y=1, z=76}, {x=45, y=7, z=76}, {x=21, y=14, z=76}, {x=44, y=23, z=76}, {x=29, y=35, z=76}, {x=38, y=40, z=76}, {x=39, y=42, z=76}, {x=33, y=58, z=76}, {x=34, y=1, z=77}, {x=21, y=7, z=77}, {x=18, y=11, z=77}, {x=26, y=23, z=77}, {x=43, y=25, z=77}, {x=41, y=32, z=77}, {x=36, y=41, z=77}, {x=39, y=47, z=77}, {x=35, y=56, z=77}, {x=35, y=1, z=78}, {x=26, y=3, z=78}, {x=34, y=3, z=78}, {x=18, y=9, z=78}, {x=27, y=23, z=78}, {x=51, y=33, z=78}, {x=41, y=37, z=78}, {x=36, y=1, z=79}, {x=25, y=2, z=79}, {x=18, y=8, z=79}, {x=15, y=10, z=79}, {x=14, y=11, z=79}, {x=27, y=23, z=79}, {x=28, y=25, z=79}, {x=45, y=32, z=79}, {x=33, y=34, z=79}, {x=34, y=34, z=79}, {x=37, y=55, z=79}, {x=40, y=62, z=79}, {x=27, y=0, z=80}, {x=31, y=18, z=80}, {x=30, y=26, z=80}, {x=34, y=61, z=80}, {x=20, y=7, z=81}, {x=51, y=7, z=81}, {x=25, y=8, z=81}, {x=53, y=8, z=81}, {x=42, y=10, z=81}, {x=56, y=12, z=81}, {x=21, y=15, z=81}, {x=37, y=28, z=81}, {x=36, y=29, z=81}, {x=37, y=29, z=81}, {x=44, y=35, z=81}, {x=22, y=7, z=82}, {x=26, y=8, z=82}, {x=29, y=8, z=82}, {x=44, y=9, z=82}, {x=42, y=10, z=82}, {x=32, y=13, z=82}, {x=13, y=14, z=82}, {x=29, y=22, z=82}, {x=31, y=25, z=82}, {x=35, y=27, z=82}, {x=27, y=60, z=82}, {x=41, y=64, z=82}, {x=20, y=8, z=83}, {x=57, y=8, z=83}, {x=24, y=9, z=83}, {x=58, y=9, z=83}, {x=36, y=22, z=83}, {x=32, y=24, z=83}, {x=47, y=8, z=84}, {x=56, y=8, z=84}, {x=59, y=11, z=84}, {x=45, y=13, z=84}, {x=58, y=13, z=84}, {x=17, y=14, z=84}, {x=23, y=14, z=84}, {x=56, y=14, z=84}, {x=29, y=19, z=84}, {x=36, y=19, z=84}, {x=27, y=59, z=84}, {x=35, y=6, z=85}, {x=9, y=8, z=85}, {x=41, y=11, z=85}, {x=50, y=13, z=85}, {x=33, y=58, z=85}, {x=34, y=58, z=85}, {x=33, y=7, z=86}, {x=18, y=10, z=86}, {x=9, y=12, z=86}, {x=41, y=12, z=87}, {x=41, y=60, z=87}, {x=9, y=2, z=88}, {x=7, y=5, z=88}, {x=5, y=10, z=88}, {x=41, y=11, z=88}, {x=62, y=11, z=88}, {x=42, y=68, z=88}, {x=37, y=6, z=89}, {x=66, y=8, z=89}, {x=9, y=10, z=89}, {x=19, y=10, z=89}, {x=58, y=12, z=89}, {x=45, y=62, z=89}, {x=7, y=5, z=90}, {x=67, y=5, z=90}, {x=7, y=9, z=90}, {x=31, y=11, z=90}, {x=62, y=11, z=90}, {x=1, y=2, z=91}, {x=5, y=5, z=91}, {x=69, y=5, z=91}, {x=62, y=8, z=91}, {x=58, y=9, z=91}, {x=63, y=10, z=91}, {x=35, y=7, z=92}, {x=62, y=9, z=92}, {x=33, y=13, z=92}, {x=36, y=62, z=92}, {x=37, y=3, z=93}, {x=37, y=6, z=93}, {x=64, y=6, z=93}, {x=32, y=10, z=93}, {x=34, y=14, z=93}, {x=39, y=57, z=93}, {x=41, y=67, z=93}, {x=33, y=9, z=94}, {x=38, y=57, z=94}, {x=41, y=69, z=94}, {x=40, y=1, z=95}, {x=34, y=7, z=97}, {x=33, y=9, z=97}, {x=33, y=10, z=102}, {x=33, y=7, z=105}, {x=35, y=9, z=107}
  617. }
  618. },
  619. {
  620. filename = TREE2_FILE,
  621. size = {x = 62, y = 65, z = 65},
  622. center = {x = 30, y = 12, z = 36},
  623. requiredIslandDepth = 16,
  624. requiredIslandRadius = 24,
  625. nodesWithConstructor = { {x=35, y=53, z=1}, {x=33, y=59, z=1}, {x=32, y=58, z=3}, {x=31, y=57, z=5}, {x=40, y=58, z=6}, {x=29, y=57, z=7}, {x=39, y=51, z=8}, {x=52, y=53, z=8}, {x=32, y=53, z=9}, {x=25, y=58, z=9}, {x=51, y=51, z=10}, {x=47, y=50, z=11}, {x=50, y=55, z=11}, {x=28, y=57, z=11}, {x=26, y=39, z=12}, {x=30, y=39, z=12}, {x=24, y=40, z=12}, {x=53, y=52, z=12}, {x=29, y=57, z=12}, {x=43, y=59, z=12}, {x=26, y=39, z=13}, {x=36, y=48, z=13}, {x=27, y=39, z=14}, {x=39, y=48, z=14}, {x=33, y=50, z=14}, {x=43, y=50, z=14}, {x=24, y=59, z=14}, {x=41, y=49, z=15}, {x=33, y=12, z=16}, {x=36, y=46, z=16}, {x=50, y=51, z=16}, {x=46, y=57, z=16}, {x=36, y=45, z=17}, {x=27, y=46, z=17}, {x=22, y=48, z=17}, {x=45, y=50, z=17}, {x=31, y=38, z=18}, {x=32, y=38, z=18}, {x=39, y=46, z=18}, {x=51, y=51, z=18}, {x=31, y=11, z=19}, {x=32, y=38, z=19}, {x=39, y=41, z=19}, {x=45, y=57, z=19}, {x=29, y=58, z=19}, {x=28, y=60, z=20}, {x=38, y=40, z=21}, {x=30, y=58, z=21}, {x=31, y=13, z=22}, {x=20, y=41, z=22}, {x=22, y=43, z=22}, {x=20, y=48, z=22}, {x=22, y=39, z=23}, {x=49, y=50, z=23}, {x=52, y=52, z=23}, {x=53, y=53, z=23}, {x=32, y=55, z=23}, {x=36, y=59, z=23}, {x=31, y=60, z=23}, {x=25, y=46, z=24}, {x=40, y=56, z=24}, {x=34, y=58, z=24}, {x=38, y=58, z=24}, {x=32, y=39, z=25}, {x=40, y=46, z=25}, {x=39, y=55, z=25}, {x=36, y=45, z=26}, {x=12, y=7, z=28}, {x=34, y=33, z=28}, {x=31, y=36, z=28}, {x=37, y=41, z=28}, {x=14, y=60, z=28}, {x=19, y=13, z=29}, {x=12, y=43, z=29}, {x=8, y=45, z=29}, {x=31, y=46, z=29}, {x=39, y=47, z=29}, {x=13, y=60, z=29}, {x=22, y=63, z=29}, {x=51, y=9, z=30}, {x=32, y=39, z=30}, {x=33, y=40, z=30}, {x=34, y=44, z=30}, {x=22, y=1, z=31}, {x=24, y=2, z=31}, {x=20, y=7, z=31}, {x=51, y=9, z=31}, {x=16, y=12, z=31}, {x=34, y=27, z=31}, {x=22, y=43, z=31}, {x=27, y=44, z=31}, {x=23, y=51, z=31}, {x=42, y=58, z=31}, {x=9, y=60, z=31}, {x=22, y=5, z=32}, {x=22, y=6, z=32}, {x=50, y=10, z=32}, {x=53, y=11, z=32}, {x=41, y=15, z=32}, {x=43, y=15, z=32}, {x=31, y=21, z=32}, {x=31, y=28, z=32}, {x=12, y=42, z=32}, {x=15, y=42, z=32}, {x=13, y=48, z=32}, {x=37, y=49, z=32}, {x=18, y=59, z=32}, {x=52, y=9, z=33}, {x=40, y=10, z=33}, {x=43, y=10, z=33}, {x=22, y=11, z=33}, {x=27, y=11, z=33}, {x=50, y=11, z=33}, {x=22, y=15, z=33}, {x=36, y=29, z=33}, {x=33, y=37, z=33}, {x=9, y=42, z=33}, {x=14, y=42, z=33}, {x=18, y=43, z=33}, {x=23, y=43, z=33}, {x=33, y=49, z=33}, {x=43, y=53, z=33}, {x=54, y=53, z=33}, {x=31, y=55, z=33}, {x=23, y=58, z=33}, {x=43, y=10, z=34}, {x=44, y=10, z=34}, {x=32, y=12, z=34}, {x=46, y=13, z=34}, {x=28, y=29, z=34}, {x=20, y=42, z=34}, {x=39, y=50, z=34}, {x=51, y=52, z=34}, {x=54, y=52, z=34}, {x=35, y=55, z=34}, {x=51, y=56, z=34}, {x=35, y=5, z=35}, {x=34, y=8, z=35}, {x=33, y=10, z=35}, {x=49, y=10, z=35}, {x=43, y=14, z=35}, {x=36, y=35, z=35}, {x=30, y=47, z=35}, {x=9, y=48, z=35}, {x=39, y=51, z=35}, {x=56, y=52, z=35}, {x=40, y=56, z=35}, {x=13, y=59, z=35}, {x=26, y=62, z=35}, {x=28, y=13, z=36}, {x=38, y=17, z=36}, {x=38, y=20, z=36}, {x=27, y=26, z=36}, {x=38, y=35, z=36}, {x=24, y=39, z=36}, {x=6, y=43, z=36}, {x=13, y=57, z=36}, {x=48, y=7, z=37}, {x=33, y=8, z=37}, {x=50, y=9, z=37}, {x=36, y=11, z=37}, {x=27, y=20, z=37}, {x=27, y=22, z=37}, {x=38, y=24, z=37}, {x=33, y=34, z=37}, {x=9, y=42, z=37}, {x=14, y=42, z=37}, {x=25, y=42, z=37}, {x=53, y=50, z=37}, {x=33, y=53, z=37}, {x=54, y=59, z=37}, {x=28, y=21, z=38}, {x=39, y=34, z=38}, {x=24, y=35, z=38}, {x=8, y=43, z=38}, {x=6, y=47, z=38}, {x=48, y=51, z=38}, {x=61, y=53, z=38}, {x=26, y=57, z=38}, {x=27, y=57, z=38}, {x=32, y=59, z=38}, {x=29, y=62, z=38}, {x=38, y=62, z=38}, {x=33, y=7, z=39}, {x=34, y=9, z=39}, {x=28, y=23, z=39}, {x=34, y=37, z=39}, {x=19, y=42, z=39}, {x=55, y=50, z=39}, {x=47, y=51, z=39}, {x=11, y=54, z=39}, {x=9, y=60, z=39}, {x=33, y=61, z=39}, {x=33, y=4, z=40}, {x=30, y=11, z=40}, {x=39, y=13, z=40}, {x=36, y=23, z=40}, {x=22, y=38, z=40}, {x=54, y=49, z=40}, {x=53, y=50, z=40}, {x=23, y=54, z=40}, {x=28, y=57, z=40}, {x=29, y=57, z=40}, {x=31, y=29, z=41}, {x=27, y=34, z=41}, {x=30, y=37, z=41}, {x=42, y=38, z=41}, {x=12, y=42, z=41}, {x=15, y=42, z=41}, {x=44, y=44, z=41}, {x=28, y=57, z=41}, {x=55, y=57, z=41}, {x=9, y=59, z=41}, {x=30, y=10, z=42}, {x=26, y=15, z=42}, {x=31, y=15, z=42}, {x=34, y=17, z=42}, {x=28, y=36, z=42}, {x=38, y=44, z=42}, {x=42, y=44, z=42}, {x=46, y=44, z=42}, {x=32, y=47, z=42}, {x=52, y=47, z=42}, {x=39, y=55, z=42}, {x=54, y=56, z=42}, {x=34, y=59, z=42}, {x=40, y=11, z=43}, {x=30, y=14, z=43}, {x=28, y=16, z=43}, {x=34, y=31, z=43}, {x=11, y=43, z=43}, {x=14, y=43, z=43}, {x=28, y=47, z=43}, {x=57, y=50, z=43}, {x=61, y=54, z=43}, {x=30, y=58, z=43}, {x=34, y=59, z=43}, {x=7, y=61, z=43}, {x=41, y=10, z=44}, {x=29, y=15, z=44}, {x=36, y=39, z=44}, {x=6, y=43, z=44}, {x=30, y=47, z=44}, {x=57, y=50, z=44}, {x=38, y=10, z=45}, {x=42, y=10, z=45}, {x=11, y=43, z=45}, {x=14, y=43, z=45}, {x=46, y=44, z=45}, {x=32, y=45, z=45}, {x=55, y=45, z=45}, {x=3, y=48, z=45}, {x=31, y=57, z=45}, {x=41, y=3, z=46}, {x=40, y=7, z=46}, {x=28, y=11, z=46}, {x=23, y=13, z=46}, {x=19, y=43, z=46}, {x=24, y=9, z=47}, {x=39, y=9, z=47}, {x=43, y=12, z=47}, {x=5, y=43, z=47}, {x=42, y=43, z=47}, {x=46, y=43, z=47}, {x=24, y=47, z=47}, {x=60, y=52, z=47}, {x=24, y=54, z=47}, {x=37, y=57, z=47}, {x=11, y=60, z=47}, {x=27, y=9, z=48}, {x=27, y=11, z=48}, {x=22, y=14, z=48}, {x=15, y=44, z=48}, {x=51, y=45, z=48}, {x=23, y=49, z=48}, {x=59, y=53, z=48}, {x=9, y=56, z=48}, {x=33, y=59, z=48}, {x=41, y=14, z=49}, {x=8, y=43, z=49}, {x=10, y=43, z=49}, {x=39, y=43, z=49}, {x=34, y=44, z=49}, {x=47, y=44, z=49}, {x=48, y=44, z=49}, {x=24, y=51, z=49}, {x=10, y=55, z=49}, {x=32, y=59, z=49}, {x=20, y=61, z=49}, {x=11, y=63, z=49}, {x=25, y=8, z=50}, {x=22, y=10, z=50}, {x=42, y=14, z=50}, {x=10, y=43, z=50}, {x=43, y=43, z=50}, {x=61, y=46, z=50}, {x=39, y=54, z=50}, {x=24, y=12, z=51}, {x=50, y=44, z=51}, {x=52, y=45, z=51}, {x=54, y=45, z=51}, {x=2, y=46, z=51}, {x=8, y=51, z=51}, {x=7, y=52, z=51}, {x=37, y=58, z=51}, {x=22, y=50, z=52}, {x=25, y=55, z=52}, {x=39, y=58, z=52}, {x=20, y=7, z=53}, {x=40, y=43, z=53}, {x=58, y=45, z=53}, {x=60, y=50, z=53}, {x=22, y=55, z=53}, {x=28, y=56, z=53}, {x=50, y=62, z=53}, {x=54, y=45, z=54}, {x=61, y=46, z=54}, {x=30, y=47, z=54}, {x=30, y=49, z=54}, {x=53, y=53, z=54}, {x=18, y=55, z=54}, {x=51, y=56, z=54}, {x=46, y=62, z=54}, {x=21, y=56, z=55}, {x=24, y=56, z=55}, {x=38, y=61, z=55}, {x=19, y=49, z=56}, {x=46, y=52, z=56}, {x=47, y=53, z=56}, {x=59, y=47, z=57}, {x=26, y=57, z=57}, {x=45, y=43, z=58}, {x=15, y=50, z=58}, {x=11, y=51, z=58}, {x=50, y=44, z=59}, {x=53, y=47, z=59}, {x=43, y=49, z=59}, {x=18, y=50, z=59}, {x=18, y=51, z=60}, {x=38, y=45, z=61}, {x=50, y=47, z=61}, {x=41, y=48, z=61} },
  626. }
  627. },
  628. MODNAME = minetest.get_current_modname() -- don't hardcode incase it's copied into other mods
  629. }
  630. -- Must be called during mod load time, as it uses minetest.register_node()
  631. -- (add an optional dependency for any mod where the tree & leaf textures might be
  632. -- sourced from, to ensure they are loaded before this is called)
  633. SkyTrees.init = function()
  634. SkyTrees.minimumIslandRadius = 100000
  635. SkyTrees.minimumIslandDepth = 100000
  636. SkyTrees.maximumYOffset = 0
  637. SkyTrees.maximumHeight = 0
  638. SkyTrees.nodeName_sideVines = interop.find_node_name(NODENAMES_VINES)
  639. SkyTrees.nodeName_hangingVine = interop.find_node_name(NODENAMES_HANGINGVINE)
  640. SkyTrees.nodeName_hangingRoot = interop.find_node_name(NODENAMES_HANGINGROOT)
  641. for i,tree in pairs(SkyTrees.schematicInfo) do
  642. local fullFilename = minetest.get_modpath(SkyTrees.MODNAME) .. DIR_DELIM .. tree.filename
  643. if not interop.file_exists(fullFilename) then
  644. -- remove the schematic from the list
  645. SkyTrees.schematicInfo[i] = nil
  646. else
  647. SkyTrees.minimumIslandRadius = math_min(SkyTrees.minimumIslandRadius, tree.requiredIslandRadius)
  648. SkyTrees.minimumIslandDepth = math_min(SkyTrees.minimumIslandDepth, tree.requiredIslandDepth)
  649. SkyTrees.maximumYOffset = math_max(SkyTrees.maximumYOffset, tree.center.y)
  650. SkyTrees.maximumHeight = math_max(SkyTrees.maximumHeight, tree.size.y)
  651. tree.theme = {}
  652. SkyTrees.schematicInfo[tree.filename] = tree -- so schematicInfo of trees can be indexed by name
  653. end
  654. end
  655. local function generate_woodTypes(nodeName_templateWood, overlay, barkoverlay, nodesuffix, description, dropsTemplateWood)
  656. local trunkNode = minetest.registered_nodes[nodeName_templateWood]
  657. local newTrunkNode = {}
  658. for key, value in pairs(trunkNode) do newTrunkNode[key] = value end
  659. newTrunkNode.name = SkyTrees.MODNAME .. ":" .. nodesuffix
  660. newTrunkNode.description = description
  661. if newTrunkNode.paramtype2 == nil then newTrunkNode.paramtype2 = "facedir" end
  662. if newTrunkNode.on_dig ~= nil and minetest.get_modpath("main") then
  663. newTrunkNode.on_dig = nil -- Crafter has special trunk auto-digging logic that doesn't make sense for giant trees
  664. end
  665. if dropsTemplateWood then
  666. newTrunkNode.drop = nodeName_templateWood
  667. if newTrunkNode.groups == nil then newTrunkNode.groups = {} end
  668. newTrunkNode.groups.not_in_creative_inventory = 1
  669. else
  670. newTrunkNode.drop = nil
  671. end
  672. local tiles = trunkNode.tiles
  673. if type(tiles) == "table" then
  674. newTrunkNode.tiles = {}
  675. for key, value in pairs(tiles) do newTrunkNode.tiles[key] = value .. overlay end
  676. else
  677. newTrunkNode.tiles = tiles .. overlay
  678. end
  679. local newBarkNode = {}
  680. for key, value in pairs(newTrunkNode) do newBarkNode[key] = value end
  681. newBarkNode.name = newBarkNode.name .. BARK_SUFFIX
  682. newBarkNode.description = S("Bark of @1", newBarkNode.description)
  683. -- .drop: leave the bark nodes dropping the trunk wood
  684. tiles = trunkNode.tiles
  685. if type(tiles) == "table" then
  686. newBarkNode.tiles = { tiles[#tiles] .. barkoverlay }
  687. end
  688. --minetest.log("info", newTrunkNode.name .. ": " .. dump(newTrunkNode))
  689. minetest.register_node(newTrunkNode.name, newTrunkNode)
  690. minetest.register_node(newBarkNode.name, newBarkNode)
  691. return newTrunkNode.name
  692. end
  693. local function generate_leafTypes(nodeName_templateLeaf, overlay, nodesuffix, description, dropsTemplateLeaf, glowVariantBrightness)
  694. local leafNode = minetest.registered_nodes[nodeName_templateLeaf]
  695. local newLeafNode = {}
  696. for key, value in pairs(leafNode) do newLeafNode[key] = value end
  697. newLeafNode.name = SkyTrees.MODNAME .. ":" .. nodesuffix
  698. newLeafNode.description = description
  699. newLeafNode.sunlight_propagates = true -- soo many leaves they otherwise blot out the sun.
  700. if dropsTemplateLeaf then
  701. newLeafNode.drop = nodeName_templateLeaf
  702. if newLeafNode.groups == nil then newLeafNode.groups = {} end
  703. newLeafNode.groups.not_in_creative_inventory = 1
  704. else
  705. newLeafNode.drop = nil
  706. end
  707. local tiles = leafNode.tiles
  708. if type(tiles) == "table" then
  709. newLeafNode.tiles = {}
  710. for key, value in pairs(tiles) do newLeafNode.tiles[key] = value .. overlay end
  711. else
  712. newLeafNode.tiles = tiles .. overlay
  713. end
  714. minetest.register_node(newLeafNode.name, newLeafNode)
  715. if glowVariantBrightness ~= nil and glowVariantBrightness > 0 and BIOLUMINESCENCE then
  716. local glowingLeafNode = {}
  717. for key, value in pairs(newLeafNode) do glowingLeafNode[key] = value end
  718. glowingLeafNode.name = newLeafNode.name .. GLOW_SUFFIX
  719. glowingLeafNode.description = S("Glowing @1", description)
  720. glowingLeafNode.light_source = glowVariantBrightness
  721. minetest.register_node(glowingLeafNode.name, glowingLeafNode)
  722. end
  723. return newLeafNode.name
  724. end
  725. local templateWood = interop.find_node_name(NODENAMES_TREEWOOD)
  726. if templateWood == 'ignore' then
  727. SkyTrees.disabled = "Could not find any tree nodes"
  728. return
  729. end
  730. local normalwood = generate_woodTypes(templateWood, "", "", "tree", S("Giant tree"), true)
  731. local darkwood = generate_woodTypes(templateWood, "^[colorize:black:205", "^[colorize:black:205", "darkwood", S("Giant Ziricote"), false)
  732. local deadwood = generate_woodTypes(templateWood, "^[colorize:#EFE6B9:110", "^[colorize:#E8D0A0:110", "deadbleachedwood", S("Dead bleached wood"), false) -- make use of the bark blocks to introduce some color variance in the tree
  733. local templateLeaf = interop.find_node_name(NODENAMES_TREELEAVES)
  734. if templateLeaf == 'ignore' then
  735. SkyTrees.disabled = "Could not find any treeleaf nodes"
  736. return
  737. end
  738. local greenleaf1 = generate_leafTypes(templateLeaf, "", "leaves", S("Leaves of a giant tree"), true) -- drops templateLeaf because these look close enough to the original leaves that we won't clutter the game & creative-menu with tiny visual variants that other recipes/parts of the game won't know about
  739. local greenleaf2 = generate_leafTypes(templateLeaf, "^[colorize:#00FF00:16", "leaves2", S("Leaves of a giant tree"), false)
  740. local greenleaf3 = generate_leafTypes(templateLeaf, "^[colorize:#90FF60:28", "leaves3", S("Leaves of a giant tree"), false)
  741. local whiteblossom1 = generate_leafTypes(templateLeaf, "^[colorize:#fffdfd:alpha", "blossom_white1", S("Blossom"), false)
  742. local whiteblossom2 = generate_leafTypes(templateLeaf, "^[colorize:#fff0f0:alpha", "blossom_white2", S("Blossom"), false)
  743. local pinkblossom = generate_leafTypes(templateLeaf, "^[colorize:#FFE3E8:alpha", "blossom_whitepink", S("Blossom"), false, 5)
  744. local sakurablossom1 = generate_leafTypes(templateLeaf, "^[colorize:#ea327c:alpha", "blossom_red", S("Sakura blossom"), false, 5)
  745. local sakurablossom2 = generate_leafTypes(templateLeaf, "^[colorize:#ffc3dd:alpha", "blossom_pink", S("Sakura blossom"), false)
  746. local wisteriaBlossom1 = generate_leafTypes(templateLeaf, "^[colorize:#8087ec:alpha", "blossom_wisteria1", S("Wisteria blossom"), false)
  747. local wisteriaBlossom2 = generate_leafTypes(templateLeaf, "^[colorize:#ccc9ff:alpha", "blossom_wisteria2", S("Wisteria blossom"), false, 7)
  748. local tree = SkyTrees.schematicInfo[TREE1_FILE]
  749. if tree ~= nil then
  750. tree.defaultThemeName = "Green foliage"
  751. tree.theme[tree.defaultThemeName] = {
  752. relativeProbability = 5,
  753. trunk = normalwood,
  754. leaves1 = greenleaf1,
  755. leaves2 = greenleaf2,
  756. leaves_special = greenleaf3,
  757. vineflags = { leaves = true, hanging_leaves = true },
  758. init = function(self, position)
  759. -- if it's hot and humid then add vines
  760. local viney = get_heat(position) >= VINES_REQUIRED_TEMPERATURE and get_humidity(position) >= VINES_REQUIRED_HUMIDITY
  761. if viney then
  762. local flagSeed = position.x * 3 + position.z + ISLANDS_SEED
  763. self.vineflags.hanging_leaves = (flagSeed % 10) <= 3 or (flagSeed % 10) >= 8
  764. self.vineflags.leaves = (flagSeed % 10) <= 5
  765. self.vineflags.bark = (flagSeed % 10) <= 2
  766. self.vineflags.hanging_bark = (flagSeed % 10) <= 1
  767. end
  768. end
  769. }
  770. tree.theme["Haunted"] = {
  771. relativeProbability = 2,
  772. trunk = darkwood,
  773. vineflags = { hanging_roots = true },
  774. hasHeart = false,
  775. hasSoil = false,
  776. init = function(self, position)
  777. -- 60% of these trees are a hanging roots variant
  778. self.vineflags.hanging_roots = (position.x * 3 + position.y + position.z + ISLANDS_SEED) % 10 < 60
  779. end
  780. }
  781. tree.theme["Dead"] = {
  782. relativeProbability = 0, -- 0 because this theme will be chosen based on location, rather than chance.
  783. trunk = deadwood,
  784. hasHeart = false
  785. }
  786. tree.theme["Sakura"] = {
  787. relativeProbability = 2,
  788. trunk = darkwood,
  789. leaves1 = sakurablossom2,
  790. leaves2 = whiteblossom2,
  791. leaves_special = sakurablossom1,
  792. init = function(self, position)
  793. -- 40% of these trees are a glowing variant
  794. self.glowing = (position.x * 3 + position.z + ISLANDS_SEED) % 10 <= 3 and BIOLUMINESCENCE
  795. self.leaves_special = sakurablossom1
  796. if self.glowing then self.leaves_special = sakurablossom1 .. GLOW_SUFFIX end
  797. end
  798. }
  799. end
  800. tree = SkyTrees.schematicInfo[TREE2_FILE]
  801. if tree ~= nil then
  802. -- copy the green leaves theme from tree1
  803. tree.defaultThemeName = "Green foliage"
  804. tree.theme[tree.defaultThemeName] = SkyTrees.schematicInfo[TREE1_FILE].theme["Green foliage"]
  805. tree.theme["Wisteria"] = {
  806. relativeProbability = 2.5,
  807. trunk = normalwood,
  808. leaves1 = greenleaf1,
  809. leaves2 = wisteriaBlossom1,
  810. leaves_special = wisteriaBlossom2,
  811. vineflags = { leaves = true, hanging_leaves = true, hanging_bark = true },
  812. init = function(self, position)
  813. -- 40% of these trees are a glowing variant
  814. self.glowing = (position.x * 3 + position.z + ISLANDS_SEED) % 10 <= 3 and BIOLUMINESCENCE
  815. self.leaves_special = wisteriaBlossom2
  816. if self.glowing then self.leaves_special = wisteriaBlossom2 .. GLOW_SUFFIX end
  817. -- if it's hot and humid then allow vines on the trunk as well
  818. self.vineflags.bark = get_heat(position) >= VINES_REQUIRED_TEMPERATURE and get_humidity(position) >= VINES_REQUIRED_HUMIDITY
  819. end
  820. }
  821. tree.theme["Blossom"] = {
  822. relativeProbability = 1.5,
  823. trunk = normalwood,
  824. leaves1 = whiteblossom1,
  825. leaves2 = whiteblossom2,
  826. leaves_special = normalwood..BARK_SUFFIX,
  827. init = function(self, position)
  828. -- 30% of these trees are a glowing variant
  829. self.glowing = (position.x * 3 + position.z + ISLANDS_SEED) % 10 <= 2 and BIOLUMINESCENCE
  830. self.leaves_special = normalwood..BARK_SUFFIX
  831. if self.glowing then self.leaves_special = pinkblossom .. GLOW_SUFFIX end
  832. end
  833. }
  834. end
  835. -- fill in any omitted fields in the themes with default values
  836. for _,treeInfo in pairs(SkyTrees.schematicInfo) do
  837. for _,theme in pairs(treeInfo.theme) do
  838. if theme.bark == nil then theme.bark = theme.trunk .. BARK_SUFFIX end
  839. if theme.leaves1 == nil then theme.leaves1 = 'ignore' end
  840. if theme.leaves2 == nil then theme.leaves2 = 'ignore' end
  841. if theme.leaves_special == nil then theme.leaves_special = theme.leaves1 end
  842. if theme.vineflags == nil then theme.vineflags = {} end
  843. if theme.relativeProbability == nil then theme.relativeProbability = 1.0 end
  844. if theme.glowing == nil then theme.glowing = false end
  845. if theme.hasSoil == nil then theme.hasSoil = true end
  846. if theme.hasHeart == nil then theme.hasHeart = true end
  847. end
  848. end
  849. -- The heart of the Tree
  850. -- The difference between a living tree and and a haunted/darkened husk
  851. --
  852. -- Ideally trees would slowly fizzlefade to/from the Haunted theme depending on
  853. -- whether a player steals or restores the heart, meaning a house hollowed out inside
  854. -- a living tree would need the heart to still be kept inside it, perhaps on its
  855. -- own pedestal (unless wanting an Addam's Family treehouse).
  856. local heartwoodTexture = minetest.registered_nodes[templateWood].tiles
  857. if type(heartwoodTexture) == "table" then heartwoodTexture = heartwoodTexture[1] end
  858. local heartwoodGlow = minetest.LIGHT_MAX -- plants can grow under the heart of the Tree
  859. if not BIOLUMINESCENCE then heartwoodGlow = 0 end -- :(
  860. minetest.register_node(
  861. SkyTrees.MODNAME .. ":HeartWood",
  862. {
  863. tiles = { heartwoodTexture },
  864. description = S("Heart of the Tree"),
  865. groups = {oddly_breakable_by_hand = 3, handy = 1},
  866. _mcl_hardness = 0.4,
  867. drawtype = "nodebox",
  868. paramtype = "light",
  869. light_source = heartwoodGlow, -- plants can grow under the heart of the Tree
  870. node_box = {
  871. type = "fixed",
  872. fixed = {
  873. --[[ Original heart
  874. {-0.38, -0.38, -0.38, 0.38, 0.38, 0.38},
  875. {0.15, 0.15, 0.15, 0.5, 0.5, 0.5},
  876. {-0.5, 0.15, 0.15, -0.15, 0.5, 0.5},
  877. {-0.5, 0.15, -0.5, -0.15, 0.5, -0.15},
  878. {0.15, 0.15, -0.5, 0.5, 0.5, -0.15},
  879. {0.15, -0.5, -0.5, 0.5, -0.15, -0.15},
  880. {-0.5, -0.5, -0.5, -0.15, -0.15, -0.15},
  881. {-0.5, -0.5, 0.15, -0.15, -0.15, 0.5},
  882. {0.15, -0.5, 0.15, 0.5, -0.15, 0.5}
  883. ]]
  884. {-0.38, -0.38, -0.38, 0.38, 0.38, 0.38},
  885. {-0.5, -0.2, -0.2, 0.5, 0.2, 0.2},
  886. {-0.2, -0.5, -0.2, 0.2, 0.5, 0.2},
  887. {-0.2, -0.2, -0.5, 0.2, 0.2, 0.5}
  888. }
  889. }
  890. }
  891. )
  892. end
  893. -- this is hack to work around how place_schematic() never invalidates its cache
  894. -- a unique schematic filename is generated for each unique theme
  895. SkyTrees.getMalleatedFilename = function(schematicInfo, themeName)
  896. -- create a unique id for the theme
  897. local theme = schematicInfo.theme[themeName]
  898. local flags = 0
  899. if theme.glowing then flags = flags + 1 end
  900. if theme.vineflags.leaves then flags = flags + 2 end
  901. if theme.vineflags.hanging_leaves then flags = flags + 4 end
  902. if theme.vineflags.bark then flags = flags + 8 end
  903. if theme.vineflags.hanging_bark then flags = flags + 16 end
  904. if theme.vineflags.hanging_roots then flags = flags + 32 end
  905. if theme.hasSoil then flags = flags + 64 end
  906. if theme.hasHeart then flags = flags + 128 end
  907. local uniqueId = themeName .. flags
  908. if schematicInfo.malleatedFilenames == nil then schematicInfo.malleatedFilenames = {} end
  909. if schematicInfo.malleatedFilenames[uniqueId] == nil then
  910. local malleationCount = 0
  911. for _ in pairs(schematicInfo.malleatedFilenames) do malleationCount = malleationCount + 1 end
  912. local malleatedFilename = minetest.get_modpath(SkyTrees.MODNAME) .. DIR_DELIM
  913. for i = 1, malleationCount do
  914. malleatedFilename = malleatedFilename .. '.' .. DIR_DELIM -- should work on both Linux and Windows
  915. end
  916. malleatedFilename = malleatedFilename .. schematicInfo.filename
  917. schematicInfo.malleatedFilenames[uniqueId] = malleatedFilename
  918. end
  919. --minetest.log("info", "Malleated file name for " .. uniqueId .. " is " .. schematicInfo.malleatedFilenames[uniqueId])
  920. return schematicInfo.malleatedFilenames[uniqueId]
  921. end
  922. -- Returns true if a tree in this location would be dead
  923. -- (checks for desert)
  924. SkyTrees.isDead = function(position)
  925. local heat = get_heat(position)
  926. local humidity = get_humidity(position)
  927. if humidity <= 10 or (humidity <= 20 and heat >= 80) then
  928. return true
  929. end
  930. local biomeId = interop.get_biome_key(position)
  931. local biome = biomes[biomeId]
  932. if biome ~= nil and biome.node_top ~= nil then
  933. local modname, nodename = interop.split_nodename(biome.node_top)
  934. if string.find(nodename, "sand") or string.find(nodename, "desert") then
  935. return true
  936. end
  937. end
  938. end
  939. -- Returns the name of a suitable theme
  940. -- Picks a theme from the schematicInfo automatically, based on the themes' relativeProbability, and location.
  941. SkyTrees.selectTheme = function(position, schematicInfo, choiceSeed)
  942. local deadThemeName = "Dead"
  943. if schematicInfo.theme[deadThemeName] ~= nil then
  944. -- Tree is dead and bleached in desert biomes
  945. if SkyTrees.isDead(position) then
  946. return deadThemeName
  947. end
  948. end
  949. if choiceSeed == nil then choiceSeed = 0 end
  950. -- Use a known PRNG implementation
  951. local prng = PcgRandom(
  952. position.x * 65732 +
  953. position.z * 729 +
  954. schematicInfo.size.x * 3 +
  955. choiceSeed
  956. )
  957. local sumProbabilities = 0
  958. for _,theme in pairs(schematicInfo.theme) do
  959. sumProbabilities = sumProbabilities + theme.relativeProbability
  960. end
  961. local selection = prng:next(0, sumProbabilities * 1000) / 1000
  962. if DEBUG_SKYTREES then minetest.log("info", "Skytrees x: "..position.x.." y: ".. position.y .. " sumProbabilities: " .. sumProbabilities .. ", selection: " .. selection) end
  963. sumProbabilities = 0
  964. for themeName,theme in pairs(schematicInfo.theme) do
  965. if selection <= sumProbabilities + theme.relativeProbability then
  966. return themeName
  967. else
  968. sumProbabilities = sumProbabilities + theme.relativeProbability
  969. end
  970. end
  971. error(SkyTrees.MODNAME .. " - SkyTrees.selectTheme failed to find a theme", 0)
  972. return schematicInfo.defaultThemeName
  973. end
  974. -- position is a vector {x, y, z}
  975. -- rotation must be either 0, 90, 180, or 270
  976. -- schematicInfo must be one of the items in SkyTrees.schematicInfo[]
  977. -- topsoil [optional] is the biome's "node_top" - the ground node of the region.
  978. SkyTrees.placeTree = function(position, rotation, schematicInfo, themeName, topsoil)
  979. if SkyTrees.disabled ~= nil then
  980. error(SkyTrees.MODNAME .. " - SkyTrees are disabled: " .. SkyTrees.disabled, 0)
  981. return
  982. end
  983. -- returns a new position vector, rotated around (0, 0) to match the schematic rotation (provided the schematic_size is correct!)
  984. local function rotatePositon(position, schematic_size, rotation)
  985. local result = vector.new(position);
  986. if rotation == 90 then
  987. result.x = position.z
  988. result.z = schematic_size.x - position.x - 1
  989. elseif rotation == 180 then
  990. result.x = schematic_size.x - position.x - 1
  991. result.z = schematic_size.z - position.z - 1
  992. elseif rotation == 270 then
  993. result.x = schematic_size.z - position.z - 1
  994. result.z = position.x
  995. end
  996. return result
  997. end
  998. local rotatedCenter = rotatePositon(schematicInfo.center, schematicInfo.size, rotation);
  999. local treePos = vector.subtract(position, rotatedCenter)
  1000. if themeName == nil then themeName = SkyTrees.selectTheme(position, schematicInfo) end
  1001. local theme = schematicInfo.theme[themeName]
  1002. if theme == nil then error(MODNAME .. ' called SkyTrees.placeTree("' .. schematicInfo.filename .. '") with invalid theme: ' .. themeName, 0) end
  1003. if theme.init ~= nil then theme.init(theme, position) end
  1004. if theme.hasSoil then
  1005. if topsoil == nil then
  1006. topsoil = 'ignore'
  1007. if minetest.get_biome_data == nil then error(SkyTrees.MODNAME .. " requires Minetest v5.0 or greater, or to have minor modifications to support v0.4.x", 0) end
  1008. local treeBiome = biomes[interop.get_biome_key(position)]
  1009. if treeBiome ~= nil and treeBiome.node_top ~= nil then topsoil = treeBiome.node_top end
  1010. end
  1011. else
  1012. topsoil = 'ignore'
  1013. end
  1014. local nodeName_heart = SkyTrees.MODNAME .. ":HeartWood"
  1015. if not theme.hasHeart then nodeName_heart = 'ignore' end
  1016. -- theme.init() may have changed the vineflags, so update the replacement node names
  1017. if theme.vineflags.hanging_leaves == true and SkyTrees.nodeName_hangingVine == 'ignore' then theme.vineflags.leaves = true end -- if there are no hanging vines then substitute side_vines
  1018. if theme.vineflags.leaves == true then theme.leaf_vines = SkyTrees.nodeName_sideVines else theme.leaf_vines = 'ignore' end
  1019. if theme.vineflags.bark == true then theme.bark_vines = SkyTrees.nodeName_sideVines else theme.bark_vines = 'ignore' end
  1020. if theme.vineflags.hanging_leaves == true then theme.hanging_leaf_vines = SkyTrees.nodeName_hangingVine else theme.hanging_leaf_vines = 'ignore' end
  1021. if theme.vineflags.hanging_bark == true then theme.hanging_bark_vines = SkyTrees.nodeName_hangingVine else theme.hanging_bark_vines = 'ignore' end
  1022. if theme.vineflags.hanging_roots == true and SkyTrees.nodeName_hangingRoot ~= 'ignore' then theme.hanging_bark_vines = SkyTrees.nodeName_hangingRoot end
  1023. local replacements = {
  1024. ['treebark\r\n\r\n~~~ Cloudlands_tree mts by Dr.Frankenstone: Amateur Arborist ~~~\r\n\r\n'] = theme.bark, -- because this node name is always replaced, it can double as space for a text header in the file.
  1025. ['default:tree'] = theme.trunk,
  1026. ['default:leaves'] = theme.leaves1,
  1027. ['leaves_alt'] = theme.leaves2,
  1028. ['leaves_special'] = theme.leaves_special,
  1029. ['leaf_vines'] = theme.leaf_vines,
  1030. ['bark_vines'] = theme.bark_vines,
  1031. ['hanging_leaf_vines'] = theme.hanging_leaf_vines,
  1032. ['hanging_bark_vines'] = theme.hanging_bark_vines,
  1033. ['default:dirt'] = topsoil,
  1034. ['heart'] = nodeName_heart
  1035. }
  1036. if minetest.global_exists("schemlib") then
  1037. -- Use schemlib instead minetest.place_schematic(), to avoid bugs in place_schematic()
  1038. local filename = minetest.get_modpath(SkyTrees.MODNAME) .. DIR_DELIM .. schematicInfo.filename
  1039. local plan_obj = schemlib.plan.new()
  1040. plan_obj:read_from_schem_file(filename, replacements)
  1041. plan_obj.data.ground_y = -1 -- prevent read_from_schem_file() from automatically adjusting the height when it encounters dirt in the schematic (SkyTrees sometimes have dirt up in their nooks)
  1042. plan_obj.data.facedir = round(rotation / 90)
  1043. rotatedCenter = plan_obj:get_world_pos(vector.add(vector.multiply(schematicInfo.center, -1), -1), position); -- this function performs the rotation I require, even if it's named/intended for something else.
  1044. plan_obj.data.anchor_pos = rotatedCenter
  1045. if DEBUG_SKYTREES then minetest.log("info", "building tree at " .. dump(position) .. "rotated to " .. dump(treePos) .. "rotatedCenter " .. dump(rotatedCenter) .. ", " .. schematicInfo.filename) end
  1046. plan_obj:set_status("build")
  1047. else -- fall back on minetest.place_schematic()
  1048. local malleatedFilename = SkyTrees.getMalleatedFilename(schematicInfo, themeName)
  1049. if DEBUG_SKYTREES then minetest.log("info", "placing tree at " .. dump(position) .. "rotated to " .. dump(treePos) .. "rotatedCenter " .. dump(rotatedCenter) .. ", " .. schematicInfo.filename) end
  1050. -- Defering minetest.place_schematic() until after the lua emerge seems to reduce the likelyhood of
  1051. -- having it draw the tree with pieces missing.
  1052. minetest.after(
  1053. 0.1,
  1054. function(treePos, malleatedFilename, rotation, replacements, schematicInfo)
  1055. minetest.place_schematic(treePos, malleatedFilename, rotation, replacements, true)
  1056. -- minetest.place_schematic() doesn't invoke node constructors, so use set_node() for any nodes requiring construction
  1057. for i, schematicCoords in pairs(schematicInfo.nodesWithConstructor) do
  1058. if rotation ~= 0 then schematicCoords = rotatePositon(schematicCoords, schematicInfo.size, rotation) end
  1059. local nodePos = vector.add(treePos, schematicCoords)
  1060. local nodeToConstruct = minetest.get_node(nodePos)
  1061. if nodeToConstruct.name == "air" or nodeToConstruct.name == nodeName_ignore then
  1062. --this is now normal - e.g. if vines are set to 'ignore' then the nodeToConstruct won't be there.
  1063. --minetest.log("error", "nodesWithConstructor["..i.."] does not match schematic " .. schematicInfo.filename .. " at " .. nodePos.x..","..nodePos.y..","..nodePos.z.." rotation "..rotation)
  1064. else
  1065. minetest.set_node(nodePos, nodeToConstruct)
  1066. end
  1067. end
  1068. end,
  1069. treePos, malleatedFilename, rotation, replacements, schematicInfo
  1070. )
  1071. end
  1072. end
  1073. end
  1074. SkyTrees.init();
  1075. --[[==============================
  1076. Initialization and Mapgen
  1077. ==============================]]--
  1078. local function init_mapgen()
  1079. -- invoke get_perlin() here, since it can't be invoked before the environment
  1080. -- is created because it uses the world's seed value.
  1081. noise_eddyField = minetest.get_perlin(noiseparams_eddyField)
  1082. noise_heightMap = minetest.get_perlin(noiseparams_heightMap)
  1083. noise_density = minetest.get_perlin(noiseparams_density)
  1084. noise_surfaceMap = minetest.get_perlin(noiseparams_surfaceMap)
  1085. noise_skyReef = minetest.get_perlin(noiseparams_skyReef)
  1086. local prng = PcgRandom(122456 + ISLANDS_SEED)
  1087. for i = 0,255 do randomNumbers[i] = prng:next(0, 0x10000) / 0x10000 end
  1088. if isMapgenV6 then
  1089. biomes["Normal"] = {node_top="mapgen_dirt_with_grass", node_filler="mapgen_dirt", node_stone="mapgen_stone"}
  1090. biomes["Desert"] = {node_top="mapgen_desert_sand", node_filler="mapgen_desert_sand", node_stone="mapgen_desert_stone"}
  1091. biomes["Jungle"] = {node_top="mapgen_dirt_with_grass", node_filler="mapgen_dirt", node_stone="mapgen_stone"}
  1092. biomes["Tundra"] = {node_top="mapgen_dirt_with_snow", node_filler="mapgen_dirt", node_stone="mapgen_stone"}
  1093. biomes["Taiga"] = {node_top="mapgen_dirt_with_snow", node_filler="mapgen_dirt", node_stone="mapgen_stone"}
  1094. else
  1095. for k,v in pairs(minetest.registered_biomes) do
  1096. biomes[minetest.get_biome_id(k)] = v;
  1097. end
  1098. end
  1099. if DEBUG then minetest.log("info", "registered biomes: " .. dump(biomes)) end
  1100. nodeId_air = minetest.get_content_id("air")
  1101. nodeId_stone = interop.find_node_id(NODENAMES_STONE)
  1102. nodeId_grass = interop.find_node_id(NODENAMES_GRASS)
  1103. nodeId_dirt = interop.find_node_id(NODENAMES_DIRT)
  1104. nodeId_water = interop.find_node_id(NODENAMES_WATER)
  1105. nodeId_ice = interop.find_node_id(NODENAMES_ICE)
  1106. nodeId_silt = interop.find_node_id(NODENAMES_SILT)
  1107. nodeId_gravel = interop.find_node_id(NODENAMES_GRAVEL)
  1108. nodeId_vine = interop.find_node_id(NODENAMES_VINES)
  1109. nodeName_vine = minetest.get_name_from_content_id(nodeId_vine)
  1110. local regionRectStr = minetest.settings:get(MODNAME .. "_limit_rect")
  1111. if type(regionRectStr) == "string" then
  1112. local minXStr, minZStr, maxXStr, maxZStr = string.match(regionRectStr, '(-?[%d%.]+)[,%s]+(-?[%d%.]+)[,%s]+(-?[%d%.]+)[,%s]+(-?[%d%.]+)')
  1113. if minXStr ~= nil then
  1114. local minX, minZ, maxX, maxZ = tonumber(minXStr), tonumber(minZStr), tonumber(maxXStr), tonumber(maxZStr)
  1115. if minX ~= nil and maxX ~= nil and minX < maxX then
  1116. region_min_x, region_max_x = minX, maxX
  1117. end
  1118. if minZ ~= nil and maxZ ~= nil and minZ < maxZ then
  1119. region_min_z, region_max_z = minZ, maxZ
  1120. end
  1121. end
  1122. end
  1123. local limitToBiomesStr = minetest.settings:get(MODNAME .. "_limit_biome")
  1124. if type(limitToBiomesStr) == "string" and string.len(limitToBiomesStr) > 0 then
  1125. limit_to_biomes = limitToBiomesStr:lower()
  1126. end
  1127. limit_to_biomes_altitude = tonumber(minetest.settings:get(MODNAME .. "_limit_biome_altitude"))
  1128. region_restrictions =
  1129. region_min_x > -32000 or region_min_z > -32000
  1130. or region_max_x < 32000 or region_max_z < 32000
  1131. or limit_to_biomes ~= nil
  1132. end
  1133. -- Updates coreList to include all cores of type coreType within the given bounds
  1134. local function addCores(coreList, coreType, x1, z1, x2, z2)
  1135. -- this function is used by the API functions, so may be invoked without our on_generated
  1136. -- being called
  1137. cloudlands.init();
  1138. for z = math_floor(z1 / coreType.territorySize), math_floor(z2 / coreType.territorySize) do
  1139. for x = math_floor(x1 / coreType.territorySize), math_floor(x2 / coreType.territorySize) do
  1140. -- Use a known PRNG implementation, to make life easier for Amidstest
  1141. local prng = PcgRandom(
  1142. x * 8973896 +
  1143. z * 7467838 +
  1144. worldSeed + 8438 + ISLANDS_SEED
  1145. )
  1146. local coresInTerritory = {}
  1147. for i = 1, coreType.coresPerTerritory do
  1148. local coreX = x * coreType.territorySize + prng:next(0, coreType.territorySize - 1)
  1149. local coreZ = z * coreType.territorySize + prng:next(0, coreType.territorySize - 1)
  1150. -- there's strong vertical and horizontal tendency in 2-octave noise,
  1151. -- so rotate it a little to avoid it lining up with the world axis.
  1152. local noiseX = ROTATE_COS * coreX - ROTATE_SIN * coreZ
  1153. local noiseZ = ROTATE_SIN * coreX + ROTATE_COS * coreZ
  1154. local eddyField = noise_eddyField:get_2d({x = noiseX, y = noiseZ})
  1155. if (math_abs(eddyField) < coreType.frequency) then
  1156. local nexusConditionMet = not coreType.requiresNexus
  1157. if not nexusConditionMet then
  1158. -- A 'nexus' is a made up name for a place where the eddyField is flat.
  1159. -- There are often many 'field lines' leading out from a nexus.
  1160. -- Like a saddle in the perlin noise the height "coreType.frequency"
  1161. local eddyField_orthA = noise_eddyField:get_2d({x = noiseX + 2, y = noiseZ})
  1162. local eddyField_orthB = noise_eddyField:get_2d({x = noiseX, y = noiseZ + 2})
  1163. if math_abs(eddyField - eddyField_orthA) + math_abs(eddyField - eddyField_orthB) < 0.02 then
  1164. nexusConditionMet = true
  1165. end
  1166. end
  1167. if nexusConditionMet then
  1168. local radius = (coreType.radiusMax + prng:next(0, coreType.radiusMax) * 2) / 3 -- give a 33%/66% weighting split between max-radius and random
  1169. local depth = (coreType.depthMax + prng:next(0, coreType.depthMax) * 2) / 2 -- ERROR!! fix this bug! should be dividing by 3. But should not change worldgen now, so adjust depthMax of islands so nothing changes when bug is fixed?
  1170. local thickness = prng:next(0, coreType.thicknessMax)
  1171. if coreX >= x1 and coreX < x2 and coreZ >= z1 and coreZ < z2 then
  1172. local spaceConditionMet = not coreType.exclusive
  1173. if not spaceConditionMet then
  1174. -- see if any other cores occupy this space, and if so then
  1175. -- either deny the core, or raise it
  1176. spaceConditionMet = true
  1177. local minDistSquared = radius * radius * .7
  1178. for _,core in ipairs(coreList) do
  1179. if core.type.radiusMax == coreType.radiusMax then
  1180. -- We've reached the cores of the current type. We can't exclude based on all
  1181. -- cores of the same type as we can't be sure neighboring territories will have been generated.
  1182. break
  1183. end
  1184. if (core.x - coreX)*(core.x - coreX) + (core.z - coreZ)*(core.z - coreZ) <= minDistSquared + core.radius * core.radius then
  1185. spaceConditionMet = false
  1186. break
  1187. end
  1188. end
  1189. if spaceConditionMet then
  1190. for _,core in ipairs(coresInTerritory) do
  1191. -- We can assume all cores of the current type are being generated in this territory,
  1192. -- so we can exclude the core if it overlaps one already in this territory.
  1193. if (core.x - coreX)*(core.x - coreX) + (core.z - coreZ)*(core.z - coreZ) <= minDistSquared + core.radius * core.radius then
  1194. spaceConditionMet = false
  1195. break
  1196. end
  1197. end
  1198. end;
  1199. end
  1200. if spaceConditionMet then
  1201. -- all conditions met, we've located a new island core
  1202. --minetest.log("Adding core "..x..","..y..","..z..","..radius);
  1203. local y = round(noise_heightMap:get_2d({x = coreX, y = coreZ}))
  1204. local newCore = {
  1205. x = coreX,
  1206. y = y,
  1207. z = coreZ,
  1208. radius = radius,
  1209. thickness = thickness,
  1210. depth = depth,
  1211. type = coreType,
  1212. }
  1213. coreList[#coreList + 1] = newCore
  1214. coresInTerritory[#coreList + 1] = newCore
  1215. end
  1216. else
  1217. -- We didn't test coreX,coreZ against x1,z1,x2,z2 immediately and save all
  1218. -- that extra work, as that would break the determinism of the prng calls.
  1219. -- i.e. if the area was approached from a different direction then a
  1220. -- territory might end up with a different list of cores.
  1221. -- TODO: filter earlier but advance prng?
  1222. end
  1223. end
  1224. end
  1225. end
  1226. end
  1227. end
  1228. end
  1229. -- removes any islands that fall outside region restrictions specified in the options
  1230. local function removeUnwantedIslands(coreList)
  1231. local testBiome = limit_to_biomes ~= nil
  1232. local get_biome_name = nil
  1233. if testBiome then
  1234. -- minetest.get_biome_name() was added in March 2018, we'll ignore the
  1235. -- limit_to_biomes option on versions of Minetest that predate this
  1236. get_biome_name = minetest.get_biome_name
  1237. testBiome = get_biome_name ~= nil
  1238. if get_biome_name == nil then
  1239. minetest.log("warning", MODNAME .. " ignoring " .. MODNAME .. "_limit_biome option as Minetest API version too early to support get_biome_name()")
  1240. limit_to_biomes = nil
  1241. end
  1242. end
  1243. for i = #coreList, 1, -1 do
  1244. local core = coreList[i]
  1245. local coreX = core.x
  1246. local coreZ = core.z
  1247. if coreX < region_min_x or coreX > region_max_x or coreZ < region_min_z or coreZ > region_max_z then
  1248. table.remove(coreList, i)
  1249. elseif testBiome then
  1250. local biomeAltitude
  1251. if (limit_to_biomes_altitude == nil) then biomeAltitude = ALTITUDE + core.y else biomeAltitude = limit_to_biomes_altitude end
  1252. local biomeName = get_biome_name(minetest.get_biome_data({x = coreX, y = biomeAltitude, z = coreZ}).biome)
  1253. if not string.match(limit_to_biomes, biomeName:lower()) then
  1254. table.remove(coreList, i)
  1255. end
  1256. end
  1257. end
  1258. end
  1259. -- gets an array of all cores which may intersect the (minp, maxp) area
  1260. -- y is ignored
  1261. cloudlands.get_island_details = function(minp, maxp)
  1262. local result = {}
  1263. for _,coreType in pairs(cloudlands.coreTypes) do
  1264. addCores(
  1265. result,
  1266. coreType,
  1267. minp.x - coreType.radiusMax,
  1268. minp.z - coreType.radiusMax,
  1269. maxp.x + coreType.radiusMax,
  1270. maxp.z + coreType.radiusMax
  1271. )
  1272. end
  1273. -- remove islands only after cores have all generated to avoid the restriction
  1274. -- settings from rearranging islands.
  1275. if region_restrictions then removeUnwantedIslands(result) end
  1276. return result;
  1277. end
  1278. cloudlands.find_nearest_island = function(x, z, search_radius)
  1279. local coreList = {}
  1280. for _,coreType in pairs(cloudlands.coreTypes) do
  1281. addCores(
  1282. coreList,
  1283. coreType,
  1284. x - (search_radius + coreType.radiusMax),
  1285. z - (search_radius + coreType.radiusMax),
  1286. x + (search_radius + coreType.radiusMax),
  1287. z + (search_radius + coreType.radiusMax)
  1288. )
  1289. end
  1290. -- remove islands only after cores have all generated to avoid the restriction
  1291. -- settings from rearranging islands.
  1292. if region_restrictions then removeUnwantedIslands(coreList) end
  1293. local result = nil
  1294. for _,core in ipairs(coreList) do
  1295. local distance = math.hypot(core.x - x, core.z - z)
  1296. if distance >= core.radius then
  1297. core.distance = 1 + distance - core.radius
  1298. else
  1299. -- distance is fractional
  1300. core.distance = distance / (core.radius + 1)
  1301. end
  1302. if result == nil or core.distance < result.distance then result = core end
  1303. end
  1304. return result;
  1305. end
  1306. -- coreList can be left as null, but if you wish to sample many heights in a small area
  1307. -- then use cloudlands.get_island_details() to get the coreList for that area and save
  1308. -- having to recalculate it during each call to get_height_at().
  1309. cloudlands.get_height_at = function(x, z, coreList)
  1310. local result, isWater = nil, false;
  1311. if coreList == nil then
  1312. local pos = {x = x, z = z}
  1313. coreList = cloudlands.get_island_details(pos, pos)
  1314. end
  1315. for _,core in ipairs(coreList) do
  1316. -- duplicates the code from renderCores() to find surface height
  1317. -- See the renderCores() version for explanatory comments
  1318. local horz_easing
  1319. local distanceSquared = (x - core.x)*(x - core.x) + (z - core.z)*(z - core.z)
  1320. local radiusSquared = core.radius * core.radius
  1321. local noise_weighting = 1
  1322. local shapeType = math_floor(core.depth + core.radius + core.x) % 5
  1323. if shapeType < 2 then -- convex, see renderCores() implementatin for comments
  1324. horz_easing = 1 - distanceSquared / radiusSquared
  1325. elseif shapeType == 2 then -- conical, see renderCores() implementatin for comments
  1326. horz_easing = 1 - math_sqrt(distanceSquared) / core.radius
  1327. else -- concave, see renderCores() implementatin for comments
  1328. local radiusRoot = math_sqrt(core.radius)
  1329. local squared = 1 - distanceSquared / radiusSquared
  1330. local distance = math_sqrt(distanceSquared)
  1331. local distance_normalized = distance / core.radius
  1332. local root = 1 - math_sqrt(distance) / radiusRoot
  1333. horz_easing = math_min(1, 0.8*distance_normalized*squared + 1.2*(1-distance_normalized)*root)
  1334. noise_weighting = 0.63
  1335. end
  1336. if core.radius + core.depth > 80 then noise_weighting = 0.6 end
  1337. if core.radius + core.depth > 120 then noise_weighting = 0.35 end
  1338. local surfaceNoise = noise_surfaceMap:get_2d({x = x, y = z})
  1339. if DEBUG_GEOMETRIC then surfaceNoise = SURFACEMAP_OFFSET end
  1340. local coreTop = ALTITUDE + core.y
  1341. local surfaceHeight = coreTop + round(surfaceNoise * 3 * (core.thickness + 1) * horz_easing)
  1342. if result == nil or math_max(coreTop, surfaceHeight) > result then
  1343. local coreBottom = math_floor(coreTop - (core.thickness + core.depth))
  1344. local yBottom = coreBottom
  1345. if result ~= nil then yBottom = math_max(yBottom, result + 1) end
  1346. for y = math_max(coreTop, surfaceHeight), yBottom, -1 do
  1347. local vert_easing = math_min(1, (y - coreBottom) / core.depth)
  1348. local densityNoise = noise_density:get3d({x = x, y = y - coreTop, z = z})
  1349. densityNoise = noise_weighting * densityNoise + (1 - noise_weighting) * DENSITY_OFFSET
  1350. if DEBUG_GEOMETRIC then densityNoise = DENSITY_OFFSET end
  1351. if densityNoise * ((horz_easing + vert_easing) / 2) >= REQUIRED_DENSITY then
  1352. result = y
  1353. isWater = surfaceNoise < 0
  1354. break
  1355. --[[abandoned because do we need to calc the bottom of ponds? It also needs the outer code refactored to work
  1356. if not isWater then
  1357. -- we've found the land height
  1358. break
  1359. else
  1360. -- find the pond bottom, since the water level is already given by (ALTITUDE + island.y)
  1361. local surfaceDensity = densityNoise * ((horz_easing + 1) / 2)
  1362. local onTheEdge = math_sqrt(distanceSquared) + 1 >= core.radius
  1363. if onTheEdge or surfaceDensity > (REQUIRED_DENSITY + core.type.pondWallBuffer) then
  1364. break
  1365. end
  1366. end]]
  1367. end
  1368. end
  1369. end
  1370. end
  1371. return result, isWater
  1372. end
  1373. local function setCoreBiomeData(core)
  1374. local pos = {x = core.x, y = ALTITUDE + core.y, z = core.z}
  1375. if LOWLAND_BIOMES then pos.y = LOWLAND_BIOME_ALTITUDE end
  1376. core.biomeId = interop.get_biome_key(pos)
  1377. core.biome = biomes[core.biomeId]
  1378. core.temperature = get_heat(pos)
  1379. core.humidity = get_humidity(pos)
  1380. if core.temperature == nil then core.temperature = 50 end
  1381. if core.humidity == nil then core.humidity = 50 end
  1382. if core.biome == nil then
  1383. -- Some games don't use the biome list, so come up with some fallbacks
  1384. core.biome = {}
  1385. core.biome.node_top = minetest.get_name_from_content_id(nodeId_grass)
  1386. core.biome.node_filler = minetest.get_name_from_content_id(nodeId_dirt)
  1387. end
  1388. end
  1389. local function addDetail_vines(decoration_list, core, data, area, minp, maxp)
  1390. if VINE_COVERAGE > 0 and nodeId_vine ~= nodeId_ignore then
  1391. local y = ALTITUDE + core.y
  1392. if y >= minp.y and y <= maxp.y then
  1393. -- if core.biome is nil then renderCores() never rendered it, which means it
  1394. -- doesn't instersect this draw region.
  1395. if core.biome ~= nil and core.humidity >= VINES_REQUIRED_HUMIDITY and core.temperature >= VINES_REQUIRED_TEMPERATURE then
  1396. local nodeId_top
  1397. local nodeId_filler
  1398. local nodeId_stoneBase
  1399. local nodeId_dust
  1400. if core.biome.node_top == nil then nodeId_top = nodeId_stone else nodeId_top = minetest.get_content_id(core.biome.node_top) end
  1401. if core.biome.node_filler == nil then nodeId_filler = nodeId_stone else nodeId_filler = minetest.get_content_id(core.biome.node_filler) end
  1402. if core.biome.node_stone == nil then nodeId_stoneBase = nodeId_stone else nodeId_stoneBase = minetest.get_content_id(core.biome.node_stone) end
  1403. if core.biome.node_dust == nil then nodeId_dust = nodeId_stone else nodeId_dust = minetest.get_content_id(core.biome.node_dust) end
  1404. local function isIsland(nodeId)
  1405. return (nodeId == nodeId_filler or nodeId == nodeId_top
  1406. or nodeId == nodeId_stoneBase or nodeId == nodeId_dust
  1407. or nodeId == nodeId_silt or nodeId == nodeId_water)
  1408. end
  1409. local function findHighestNodeFace(y, solidIndex, emptyIndex)
  1410. -- return the highest y value (or maxp.y) where solidIndex is part of an island
  1411. -- and emptyIndex is not
  1412. local yOffset = 1
  1413. while y + yOffset <= maxp.y and isIsland(data[solidIndex + yOffset * area.ystride]) and not isIsland(data[emptyIndex + yOffset * area.ystride]) do
  1414. yOffset = yOffset + 1
  1415. end
  1416. return y + yOffset - 1
  1417. end
  1418. local radius = round(core.radius)
  1419. local xCropped = math_min(maxp.x, math_max(minp.x, core.x))
  1420. local zStart = math_max(minp.z, core.z - radius)
  1421. local vi = area:index(xCropped, y, zStart)
  1422. for z = 0, math_min(maxp.z, core.z + radius) - zStart do
  1423. local searchIndex = vi + z * area.zstride
  1424. if isIsland(data[searchIndex]) then
  1425. -- add vines to east face
  1426. if randomNumbers[(zStart + z + y) % 256] <= VINE_COVERAGE then
  1427. for x = xCropped + 1, maxp.x do
  1428. if not isIsland(data[searchIndex + 1]) then
  1429. local yhighest = findHighestNodeFace(y, searchIndex, searchIndex + 1)
  1430. decoration_list[#decoration_list + 1] = {pos={x=x, y=yhighest, z= zStart + z}, node={name = nodeName_vine, param2 = 3}}
  1431. break
  1432. end
  1433. searchIndex = searchIndex + 1
  1434. end
  1435. end
  1436. -- add vines to west face
  1437. if randomNumbers[(zStart + z + y + 128) % 256] <= VINE_COVERAGE then
  1438. searchIndex = vi + z * area.zstride
  1439. for x = xCropped - 1, minp.x, -1 do
  1440. if not isIsland(data[searchIndex - 1]) then
  1441. local yhighest = findHighestNodeFace(y, searchIndex, searchIndex - 1)
  1442. decoration_list[#decoration_list + 1] = {pos={x=x, y=yhighest, z= zStart + z}, node={name = nodeName_vine, param2 = 2}}
  1443. break
  1444. end
  1445. searchIndex = searchIndex - 1
  1446. end
  1447. end
  1448. end
  1449. end
  1450. local zCropped = math_min(maxp.z, math_max(minp.z, core.z))
  1451. local xStart = math_max(minp.x, core.x - radius)
  1452. local zstride = area.zstride
  1453. vi = area:index(xStart, y, zCropped)
  1454. for x = 0, math_min(maxp.x, core.x + radius) - xStart do
  1455. local searchIndex = vi + x
  1456. if isIsland(data[searchIndex]) then
  1457. -- add vines to north face (make it like moss - grows better on the north side)
  1458. if randomNumbers[(xStart + x + y) % 256] <= (VINE_COVERAGE * 1.2) then
  1459. for z = zCropped + 1, maxp.z do
  1460. if not isIsland(data[searchIndex + zstride]) then
  1461. local yhighest = findHighestNodeFace(y, searchIndex, searchIndex + zstride)
  1462. decoration_list[#decoration_list + 1] = {pos={x=xStart + x, y=yhighest, z=z}, node={name = nodeName_vine, param2 = 5}}
  1463. break
  1464. end
  1465. searchIndex = searchIndex + zstride
  1466. end
  1467. end
  1468. -- add vines to south face (make it like moss - grows better on the north side)
  1469. if randomNumbers[(xStart + x + y + 128) % 256] <= (VINE_COVERAGE * 0.8) then
  1470. searchIndex = vi + x
  1471. for z = zCropped - 1, minp.z, -1 do
  1472. if not isIsland(data[searchIndex - zstride]) then
  1473. local yhighest = findHighestNodeFace(y, searchIndex, searchIndex - zstride)
  1474. decoration_list[#decoration_list + 1] = {pos={x=xStart + x, y=yhighest, z=z}, node={name = nodeName_vine, param2 = 4}}
  1475. break
  1476. end
  1477. searchIndex = searchIndex - zstride
  1478. end
  1479. end
  1480. end
  1481. end
  1482. end
  1483. end
  1484. end
  1485. end
  1486. -- A rare formation of rocks circling or crowning an island
  1487. -- returns true if voxels were changed
  1488. local function addDetail_skyReef(decoration_list, core, data, area, minp, maxp)
  1489. local coreTop = ALTITUDE + core.y
  1490. local overdrawTop = maxp.y + OVERDRAW
  1491. local reefAltitude = math_floor(coreTop - 1 - core.thickness / 2)
  1492. local reefMaxHeight = 12
  1493. local reefMaxUnderhang = 4
  1494. if (maxp.y < reefAltitude - reefMaxUnderhang) or (minp.y > reefAltitude + reefMaxHeight) then
  1495. --no reef here
  1496. return false
  1497. end
  1498. local isReef = core.radius < core.type.radiusMax * 0.4 -- a reef can't extend beyond radiusMax, so needs a small island
  1499. local isAtoll = core.radius > core.type.radiusMax * 0.8
  1500. if not (isReef or isAtoll) then return false end
  1501. local fastHash = 3
  1502. fastHash = (37 * fastHash) + core.x
  1503. fastHash = (37 * fastHash) + core.z
  1504. fastHash = (37 * fastHash) + math_floor(core.radius)
  1505. fastHash = (37 * fastHash) + math_floor(core.depth)
  1506. if ISLANDS_SEED ~= 1000 then fastHash = (37 * fastHash) + ISLANDS_SEED end
  1507. local rarityAdj = 1
  1508. if core.type.requiresNexus and isAtoll then rarityAdj = 4 end -- humongous islands are very rare, and look good as an atoll
  1509. if (REEF_RARITY * rarityAdj * 1000) < math_floor((math_abs(fastHash)) % 1000) then return false end
  1510. local coreX = core.x --save doing a table lookup in the loop
  1511. local coreZ = core.z --save doing a table lookup in the loop
  1512. -- Use a known PRNG implementation
  1513. local prng = PcgRandom(
  1514. coreX * 8973896 +
  1515. coreZ * 7467838 +
  1516. worldSeed + 32564
  1517. )
  1518. local reefUnderhang
  1519. local reefOuterRadius = math_floor(core.type.radiusMax)
  1520. local reefInnerRadius = prng:next(core.type.radiusMax * 0.5, core.type.radiusMax * 0.7)
  1521. local reefWidth = reefOuterRadius - reefInnerRadius
  1522. local noiseOffset = 0
  1523. if isReef then
  1524. reefMaxHeight = round((core.thickness + 4) / 2)
  1525. reefUnderhang = round(reefMaxHeight / 2)
  1526. noiseOffset = -0.1
  1527. end
  1528. if isAtoll then
  1529. -- a crown attached to the island
  1530. reefOuterRadius = math_floor(core.radius * 0.8)
  1531. reefWidth = math_max(4, math_floor(core.radius * 0.15))
  1532. reefInnerRadius = reefOuterRadius - reefWidth
  1533. reefUnderhang = 0
  1534. if maxp.y < reefAltitude - reefUnderhang then return end -- no atoll here
  1535. end
  1536. local reefHalfWidth = reefWidth / 2
  1537. local reefMiddleRadius = (reefInnerRadius + reefOuterRadius) / 2
  1538. local reefOuterRadiusSquared = reefOuterRadius * reefOuterRadius
  1539. local reefInnerRadiusSquared = reefInnerRadius * reefInnerRadius
  1540. local reefMiddleRadiusSquared = reefMiddleRadius * reefMiddleRadius
  1541. local reefHalfWidthSquared = reefHalfWidth * reefHalfWidth
  1542. -- get the biome details for this core
  1543. local nodeId_first
  1544. local nodeId_second
  1545. local nodeId_top
  1546. local nodeId_filler
  1547. if core.biome == nil then setCoreBiomeData(core) end -- We can't assume the core biome has already been resolved, core might not have been big enough to enter the draw region
  1548. if core.biome.node_top == nil then nodeId_top = nodeId_stone else nodeId_top = minetest.get_content_id(core.biome.node_top) end
  1549. if core.biome.node_filler == nil then nodeId_filler = nodeId_stone else nodeId_filler = minetest.get_content_id(core.biome.node_filler) end
  1550. if core.biome.node_dust ~= nil then
  1551. nodeId_first = minetest.get_content_id(core.biome.node_dust)
  1552. nodeId_second = nodeId_top
  1553. else
  1554. nodeId_first = nodeId_top
  1555. nodeId_second = nodeId_filler
  1556. end
  1557. local zStart = round(math_max(core.z - reefOuterRadius, minp.z))
  1558. local zStop = round(math_min(core.z + reefOuterRadius, maxp.z))
  1559. local xStart = round(math_max(core.x - reefOuterRadius, minp.x))
  1560. local xStop = round(math_min(core.x + reefOuterRadius, maxp.x))
  1561. local yCenter = math_min(math_max(reefAltitude, minp.y), maxp.y)
  1562. local pos = {}
  1563. local dataBufferIndex = area:index(xStart, yCenter, zStart)
  1564. local vi = -1
  1565. for z = zStart, zStop do
  1566. local zDistSquared = (z - coreZ) * (z - coreZ)
  1567. pos.y = z
  1568. for x = xStart, xStop do
  1569. local distanceSquared = (x - coreX) * (x - coreX) + zDistSquared
  1570. if distanceSquared < reefOuterRadiusSquared and distanceSquared > reefInnerRadiusSquared then
  1571. pos.x = x
  1572. local offsetEase = math_abs(distanceSquared - reefMiddleRadiusSquared) / reefHalfWidthSquared
  1573. local fineNoise = noise_skyReef:get_2d(pos)
  1574. local reefNoise = (noiseOffset* offsetEase) + fineNoise + 0.2 * noise_surfaceMap:get_2d(pos)
  1575. if (reefNoise > 0) then
  1576. local distance = math_sqrt(distanceSquared)
  1577. local ease = 1 - math_abs(distance - reefMiddleRadius) / reefHalfWidth
  1578. local yStart = math_max(math_floor(reefAltitude - ease * fineNoise * reefUnderhang), minp.y)
  1579. local yStop = math_min(math_floor(reefAltitude + ease * reefNoise * reefMaxHeight), overdrawTop)
  1580. for y = yStart, yStop do
  1581. vi = dataBufferIndex + (y - yCenter) * area.ystride
  1582. if data[vi] == nodeId_air then
  1583. if y == yStop then
  1584. data[vi] = nodeId_first
  1585. elseif y == yStop - 1 then
  1586. data[vi] = nodeId_second
  1587. else
  1588. data[vi] = nodeId_filler
  1589. end
  1590. end
  1591. end
  1592. end
  1593. end
  1594. dataBufferIndex = dataBufferIndex + 1
  1595. end
  1596. dataBufferIndex = dataBufferIndex + area.zstride - (xStop - xStart + 1)
  1597. end
  1598. return vi >= 0
  1599. end
  1600. -- A rarely occuring giant tree growing from the center of the island
  1601. -- returns true if tree was added
  1602. local function addDetail_skyTree(decoration_list, core, minp, maxp)
  1603. if (core.radius < SkyTrees.minimumIslandRadius) or (core.depth < SkyTrees.minimumIslandDepth) then
  1604. --no tree here
  1605. return false
  1606. end
  1607. local coreTop = ALTITUDE + core.y
  1608. local treeAltitude = math_floor(coreTop + core.thickness)
  1609. if (maxp.y < treeAltitude - SkyTrees.maximumYOffset) or (minp.y > treeAltitude + SkyTrees.maximumHeight) then
  1610. --no tree here
  1611. return false
  1612. elseif SkyTrees.disabled ~= nil then
  1613. -- can't find nodes/textures in this game that are needed to build trees
  1614. return false
  1615. end
  1616. local coreX = core.x --save doing a table lookups
  1617. local coreZ = core.z --save doing a table lookups
  1618. local fastHash = 3
  1619. fastHash = (37 * fastHash) + coreX
  1620. fastHash = (37 * fastHash) + coreZ
  1621. fastHash = (37 * fastHash) + math_floor(core.radius)
  1622. fastHash = (37 * fastHash) + math_floor(core.depth)
  1623. fastHash = (37 * fastHash) + ISLANDS_SEED
  1624. fastHash = (37 * fastHash) + 76276 -- to keep this probability distinct from reefs and atols
  1625. if (TREE_RARITY * 1000) < math_floor((math_abs(fastHash)) % 1000) then return false end
  1626. -- choose a tree that will fit on the island
  1627. local tree
  1628. local skipLargeTree = (fastHash % 10) < 3 -- to allow small trees a chance to spawn on large islands
  1629. if skipLargeTree then
  1630. if SkyTrees.isDead({x = coreX, y = treeAltitude, z = coreZ}) then
  1631. -- small tree currently doesn't have a dead theme, so don't skip the large tree
  1632. skipLargeTree = false
  1633. end
  1634. end
  1635. for i, treeType in pairs(SkyTrees.schematicInfo) do
  1636. if i == 1 and skipLargeTree then
  1637. -- 'continue', to allow small trees a chance to spawn on large islands
  1638. elseif (core.radius >= treeType.requiredIslandRadius) and (core.depth >= treeType.requiredIslandDepth) then
  1639. tree = treeType
  1640. break
  1641. end
  1642. end
  1643. local maxOffsetFromCenter = core.radius - (tree.requiredIslandRadius - 4); -- 4 is an arbitrary number, to allow trees to get closer to the edge
  1644. -- Use a known PRNG implementation
  1645. local prng = PcgRandom(
  1646. coreX * 8973896 +
  1647. coreZ * 7467838 +
  1648. worldSeed + 43786
  1649. )
  1650. local treeAngle = 90 * prng:next(0, 3)
  1651. local treePos = {
  1652. x = coreX + math_floor((prng:next(-maxOffsetFromCenter, maxOffsetFromCenter) + prng:next(-maxOffsetFromCenter, maxOffsetFromCenter)) / 2),
  1653. y = treeAltitude,
  1654. z = coreZ + math_floor((prng:next(-maxOffsetFromCenter, maxOffsetFromCenter) + prng:next(-maxOffsetFromCenter, maxOffsetFromCenter)) / 2)
  1655. }
  1656. if minetest.global_exists("schemlib") then
  1657. -- This check is skipped when not using schemlib, because while redrawing the tree multiple times - every time a chunk it
  1658. -- touches gets emitted - might be slower, it helps work around the bugs in minetest.place_schematic() where large schematics
  1659. -- are spawned incompletely.
  1660. -- The bug in question: https://forum.minetest.net/viewtopic.php?f=6&t=22136
  1661. -- (it isn't an issue if schemlib is used)
  1662. if (maxp.y < treePos.y) or (minp.y > treePos.y) or (maxp.x < treePos.x) or (minp.x > treePos.x) or (maxp.z < treePos.z) or (minp.z > treePos.z) then
  1663. -- Now that we know the exact position of the tree, we know it's spawn point is not in this chunk.
  1664. -- In the interests of only drawing trees once, we only invoke placeTree when the chunk containing treePos is emitted.
  1665. return false
  1666. end
  1667. end
  1668. if tree.theme["Dead"] == nil then
  1669. if SkyTrees.isDead(treePos) then
  1670. -- Trees in this location should be dead, but this tree doesn't have a dead theme, so don't put a tree here
  1671. return false
  1672. end
  1673. end
  1674. if core.biome == nil then setCoreBiomeData(core) end -- We shouldn't assume the core biome has already been resolved, it might be below the emerged chunk and unrendered
  1675. if core.biome.node_top == nil then
  1676. -- solid stone isn't fertile enough for giant trees, and there's a solid stone biome in MT-Game: tundra_highland
  1677. return false
  1678. end
  1679. if DEBUG_SKYTREES then minetest.log("info", "core x: "..coreX.." y: ".. coreZ .. " treePos: " .. treePos.x .. ", y: " .. treePos.y) end
  1680. SkyTrees.placeTree(treePos, treeAngle, tree, nil, core.biome.node_top)
  1681. return true;
  1682. end
  1683. ------------------------------------------------------------------------------
  1684. -- Secrets section
  1685. ------------------------------------------------------------------------------
  1686. -- We might not need this stand-in cobweb, but unless we go overboard on listing many
  1687. -- optional dependencies we won't know whether there's a proper cobweb available to
  1688. -- use until after it's too late to register this one.
  1689. local nodeName_standinCobweb = MODNAME .. ":cobweb"
  1690. minetest.register_node(
  1691. nodeName_standinCobweb,
  1692. {
  1693. tiles = {
  1694. -- [Ab]Use the crack texture to avoid needing to include a cobweb texture
  1695. -- crack_anylength.png is required by the engine, so all games will have it.
  1696. "crack_anylength.png^[verticalframe:5:4^[brighten"
  1697. },
  1698. description = S("Cobweb"),
  1699. groups = {snappy = 3, liquid = 3, flammable = 3, not_in_creative_inventory = 1},
  1700. drawtype = "plantlike",
  1701. walkable = false,
  1702. liquid_viscosity = 8,
  1703. liquidtype = "source",
  1704. liquid_alternative_flowing = nodeName_standinCobweb,
  1705. liquid_alternative_source = nodeName_standinCobweb,
  1706. liquid_renewable = false,
  1707. liquid_range = 0,
  1708. sunlight_propagates = true,
  1709. paramtype = "light"
  1710. }
  1711. )
  1712. local nodeName_egg = "secret:fossilized_egg"
  1713. local eggTextureBaseName = interop.find_node_texture({"default:jungleleaves", "mcl_core:jungleleaves", "ethereal:frost_leaves", "main:leaves"})
  1714. -- [Ab]Use a leaf texture. Originally this was to avoid needing to include an egg texture (extra files) and
  1715. -- exposing that the mod contains secrets, however both those reasons are obsolete and the mod could have textures
  1716. -- added in future
  1717. local eggTextureName = eggTextureBaseName.."^[colorize:#280040E0^[noalpha"
  1718. -- Since "secret:fossilized_egg" doesn't use this mod's name for the prefix, we can't assume
  1719. -- another mod isn't also using/providing it
  1720. if minetest.registered_nodes[nodeName_egg] == nil then
  1721. local fossilSounds = nil
  1722. local nodeName_stone = interop.find_node_name(NODENAMES_STONE)
  1723. if nodeName_stone ~= nodeName_ignore then fossilSounds = minetest.registered_nodes[nodeName_stone].sounds end
  1724. minetest.register_node(
  1725. ":"..nodeName_egg,
  1726. {
  1727. tiles = { eggTextureName },
  1728. description = S("Fossilized Egg"),
  1729. groups = {
  1730. oddly_breakable_by_hand = 3, -- MTG
  1731. handy = 1, -- MCL
  1732. stone = 1, -- Crafter needs to know the material in order to be breakable by hand
  1733. not_in_creative_inventory = 1
  1734. },
  1735. _mcl_hardness = 0.4,
  1736. sounds = fossilSounds,
  1737. drawtype = "nodebox",
  1738. paramtype = "light",
  1739. node_box = {
  1740. type = "fixed",
  1741. fixed = {
  1742. {-0.066666, -0.5, -0.066666, 0.066666, 0.5, 0.066666}, -- column1
  1743. {-0.133333, -0.476667, -0.133333, 0.133333, 0.42, 0.133333}, -- column2
  1744. {-0.2, -0.435, -0.2, 0.2, 0.31, 0.2 }, -- column3
  1745. {-0.2, -0.36, -0.28, 0.2, 0.16667, 0.28 }, -- side1
  1746. {-0.28, -0.36, -0.2, 0.28, 0.16667, 0.2 } -- side2
  1747. }
  1748. }
  1749. }
  1750. )
  1751. end
  1752. -- Allow the player to craft their egg into an egg in a display case
  1753. local nodeName_eggDisplay = nodeName_egg .. "_display"
  1754. local nodeName_frameGlass = interop.find_node_name(NODENAMES_FRAMEGLASS)
  1755. local woodTexture = interop.find_node_texture(NODENAMES_WOOD)
  1756. local frameTexture = nil
  1757. if woodTexture ~= nil then
  1758. -- perhaps it's time for cloudlands to contain textures.
  1759. frameTexture = "([combine:16x16:0,0="..woodTexture.."\\^[colorize\\:black\\:170:1,1="..woodTexture.."\\^[colorize\\:#0F0\\:255\\^[resize\\:14x14^[makealpha:0,255,0)"
  1760. end
  1761. -- Since "secret:fossilized_egg_display" doesn't use this mod's name as the prefix, we shouldn't
  1762. -- assume another mod isn't also using/providing it.
  1763. if frameTexture ~= nil and nodeName_frameGlass ~= nodeName_ignore and minetest.registered_nodes[nodeName_eggDisplay] == nil then
  1764. minetest.register_node(
  1765. ":"..nodeName_eggDisplay,
  1766. {
  1767. tiles = { eggTextureName .. "^" .. frameTexture },
  1768. description = S("Fossil Display"),
  1769. groups = {
  1770. oddly_breakable_by_hand = 3,
  1771. glass = 1, -- Crafter needs to know the material in order to be breakable by hand
  1772. not_in_creative_inventory = 1},
  1773. _mcl_hardness = 0.2,
  1774. drop = "",
  1775. sounds = minetest.registered_nodes[nodeName_frameGlass].sounds,
  1776. drawtype = "nodebox",
  1777. paramtype = "light",
  1778. node_box = {
  1779. type = "fixed",
  1780. fixed = {
  1781. {-0.066666, -0.5, -0.066666, 0.066666, 0.4375, 0.066666}, -- column1
  1782. {-0.133333, -0.5, -0.133333, 0.133333, 0.375, 0.133333}, -- column2
  1783. {-0.2, -0.4375, -0.2, 0.2, 0.285, 0.2 }, -- column3
  1784. {-0.2, -0.36, -0.28, 0.2, 0.14, 0.28 }, -- side1
  1785. {-0.28, -0.36, -0.2, 0.28, 0.14, 0.2 }, -- side2
  1786. -- corner frame (courtesy of NodeBox Editor Abuse mod)
  1787. {-0.4375, 0.4375, 0.4375, 0.4375, 0.5, 0.5},
  1788. {-0.4375, -0.5, 0.4375, 0.4375, -0.4375, 0.5},
  1789. {-0.5, -0.5, 0.4375, -0.4375, 0.5, 0.5},
  1790. {0.4375, -0.5, 0.4375, 0.5, 0.5, 0.5},
  1791. {-0.5, 0.4375, -0.4375, -0.4375, 0.5, 0.4375},
  1792. {-0.5, -0.5, -0.4375, -0.4375, -0.4375, 0.4375},
  1793. {0.4375, 0.4375, -0.4375, 0.5, 0.5, 0.4375},
  1794. {0.4375, -0.5, -0.4375, 0.5, -0.4375, 0.4375},
  1795. {-0.5, 0.4375, -0.5, 0.5, 0.5, -0.4375},
  1796. {-0.5, -0.5, -0.5, 0.5, -0.4375, -0.4375},
  1797. {0.4375, -0.4375, -0.5, 0.5, 0.4375, -0.4375},
  1798. {-0.5, -0.4375, -0.5, -0.4375, 0.4375, -0.4375}
  1799. }
  1800. },
  1801. after_destruct = function(pos,node)
  1802. minetest.set_node(pos, {name = nodeName_egg, param2 = node.param2})
  1803. end,
  1804. }
  1805. )
  1806. if minetest.get_modpath("xpanes") ~= nil then
  1807. minetest.register_craft({
  1808. output = nodeName_eggDisplay,
  1809. recipe = {
  1810. {"group:stick", "group:pane", "group:stick"},
  1811. {"group:pane", nodeName_egg, "group:pane"},
  1812. {"group:stick", "group:pane", "group:stick"}
  1813. }
  1814. })
  1815. else
  1816. -- Game doesn't have glass panes, so just use glass
  1817. minetest.register_craft({
  1818. output = nodeName_eggDisplay,
  1819. recipe = {
  1820. {"group:stick", nodeName_frameGlass, "group:stick"},
  1821. {nodeName_frameGlass, nodeName_egg, nodeName_frameGlass},
  1822. {"group:stick", nodeName_frameGlass, "group:stick"}
  1823. }
  1824. })
  1825. end
  1826. end
  1827. local nodeId_egg = minetest.get_content_id(nodeName_egg)
  1828. local nodeId_airStandIn = minetest.get_content_id(interop.register_clone("air"))
  1829. -- defer assigning the following until all mods are loaded
  1830. local nodeId_bed_top
  1831. local nodeId_bed_bottom
  1832. local nodeId_torch
  1833. local nodeId_chest
  1834. local nodeId_bookshelf
  1835. local nodeId_junk
  1836. local nodeId_anvil
  1837. local nodeId_workbench
  1838. local nodeId_cobweb
  1839. local nodeName_bookshelf
  1840. local isMineCloneBookshelf
  1841. local function addDetail_secrets(decoration_list, core, data, area, minp, maxp)
  1842. -- if core.biome is nil then renderCores() never rendered it, which means it
  1843. -- doesn't instersect this draw region.
  1844. if core.biome ~= nil and core.radius > 18 and core.depth > 20 and core.radius + core.depth > 60 then
  1845. local territoryX = math_floor(core.x / core.type.territorySize)
  1846. local territoryZ = math_floor(core.z / core.type.territorySize)
  1847. local isPolarOutpost = (core.temperature <= 5) and (core.x % 3 == 0) and noise_surfaceMap:get_2d({x = core.x, y = core.z - 8}) >= 0 --make sure steps aren't under a pond
  1848. local isAncientBurrow = core.humidity >= 60 and core.temperature >= 50
  1849. -- only allow a checkerboard pattern of territories to help keep the secrets
  1850. -- spread out, rather than bunching up too much with climate
  1851. if ((territoryX + territoryZ) % 2 == 0) and (isPolarOutpost or isAncientBurrow) then
  1852. local burrowRadius = 7
  1853. local burrowHeight = 5
  1854. local burrowDepth = 12
  1855. local burrowFloor = ALTITUDE + core.y - burrowDepth
  1856. local radiusSquared = burrowRadius * burrowRadius
  1857. local function carve(originp, destp, pattern, height, floorId, floorDistance)
  1858. local direction = vector.direction(originp, destp)
  1859. local vineSearchDirection = {}
  1860. if direction.x > 0 then vineSearchDirection.x = -1 else vineSearchDirection.x = 1 end
  1861. if direction.z > 0 then vineSearchDirection.z = -1 else vineSearchDirection.z = 1 end
  1862. local vinePlacements = {}
  1863. local function placeVine(vi, pos, only_place_on_nodeId)
  1864. if data[vi] == nodeId_air then
  1865. local faces = {}
  1866. local facing
  1867. local function vineCanGrowOnIt(node_id)
  1868. return node_id ~= nodeId_air and node_id ~= nodeId_airStandIn and (node_id == only_place_on_nodeId or only_place_on_nodeId == nil)
  1869. end
  1870. if vineCanGrowOnIt(data[vi + vineSearchDirection.x]) and pos.x + vineSearchDirection.x >= minp.x and pos.x + vineSearchDirection.x <= maxp.x then
  1871. if vineSearchDirection.x > 0 then facing = 2 else facing = 3 end
  1872. faces[#faces + 1] = {solid_vi = vi + vineSearchDirection.x, facing = facing}
  1873. end
  1874. if vineCanGrowOnIt(data[vi + vineSearchDirection.z * area.zstride]) and pos.z + vineSearchDirection.z >= minp.z and pos.z + vineSearchDirection.z <= maxp.z then
  1875. if vineSearchDirection.z > 0 then facing = 4 else facing = 5 end
  1876. faces[#faces + 1] = {solid_vi = vi + vineSearchDirection.z * area.zstride, facing = facing}
  1877. end
  1878. local faceInfo = nil
  1879. if #faces == 1 then
  1880. faceInfo = faces[1]
  1881. elseif #faces == 2 then
  1882. local ratio = math.abs(direction.x) / (math.abs(direction.x) + math.abs(direction.z))
  1883. if randomNumbers[(pos.x + pos.y + pos.z) % 256] <= ratio then faceInfo = faces[1] else faceInfo = faces[2] end
  1884. end
  1885. if faceInfo ~= nil
  1886. and (only_place_on_nodeId == nil or only_place_on_nodeId == data[faceInfo.solid_vi])
  1887. and (data[faceInfo.solid_vi] ~= nodeId_airStandIn) then
  1888. -- find the highest y value (or maxp.y) where solid_vi is solid
  1889. -- and vi is not
  1890. local solid_vi = faceInfo.solid_vi
  1891. local yOffset = 1
  1892. while (pos.y + yOffset <= maxp.y + 1)
  1893. and (data[solid_vi + yOffset * area.ystride] ~= nodeId_air)
  1894. and (data[vi + yOffset * area.ystride] == nodeId_air)
  1895. and (only_place_on_nodeId == nil or only_place_on_nodeId == data[solid_vi + yOffset * area.ystride]) do
  1896. yOffset = yOffset + 1
  1897. end
  1898. -- defer final vine placement until all nodes have been carved
  1899. vinePlacements[#vinePlacements + 1] = function(decoration_list)
  1900. -- retest that the vine is still going in air and still attached to a solid node
  1901. local solidNode = data[solid_vi + (yOffset - 1) * area.ystride]
  1902. if solidNode ~= nodeId_airStandIn and solidNode ~= nodeId_air and data[vi] == nodeId_air then
  1903. decoration_list[#decoration_list + 1] = {pos={x=pos.x, y=pos.y + yOffset - 1, z=pos.z}, node={name = nodeName_vine, param2 = faceInfo.facing}}
  1904. end
  1905. end
  1906. end
  1907. end
  1908. end
  1909. local stampedIndexes = {}
  1910. local function stamp(pos, pattern, height, node_id, isAir_callback)
  1911. local callbackClosures = {}
  1912. local index = -1
  1913. for y = pos.y, pos.y + height - 1 do
  1914. if y >= minp.y and y <= maxp.y then
  1915. if index == -1 then index = area:index(pos.x, y, pos.z) else index = index + area.ystride end
  1916. for _,voxel in ipairs(pattern) do
  1917. local x = pos.x + voxel.x
  1918. local z = pos.z + voxel.z
  1919. if x >= minp.x and x <= maxp.x and z >= minp.z and z <= maxp.z then
  1920. local vi = index + voxel.x + voxel.z * area.zstride
  1921. if data[vi] == nodeId_air then
  1922. if isAir_callback ~= nil then
  1923. callbackClosures[#callbackClosures + 1] = function() isAir_callback(pos, vi, x, y, z) end
  1924. end
  1925. else
  1926. data[vi] = node_id
  1927. stampedIndexes[#stampedIndexes + 1] = vi
  1928. end
  1929. end
  1930. end
  1931. end
  1932. end
  1933. for _,callback in ipairs(callbackClosures) do callback() end
  1934. end
  1935. local function excavate(pos, add_floor, add_vines, add_cobwebs)
  1936. local function onAirNode(stampPos, node_vi, node_x, node_y, node_z)
  1937. if node_y > stampPos.y and node_y + 1 <= maxp.y then
  1938. -- place vines above the entrance, for concealment
  1939. placeVine(node_vi + area.ystride, {x=node_x, y=node_y + 1, z=node_z})
  1940. else
  1941. -- place vines on the floor - perhaps explorers can climb to the burrow
  1942. placeVine(node_vi, {x=node_x, y=node_y, z=node_z}, floorId)
  1943. end
  1944. end
  1945. local onAirNodeCallback = onAirNode
  1946. local fill = nodeId_airStandIn
  1947. if not add_vines or nodeId_vine == nodeId_ignore then onAirNodeCallback = nil end
  1948. if add_cobwebs and nodeId_cobweb ~= nodeId_ignore then fill = nodeId_cobweb end
  1949. stamp(pos, pattern, height, fill, onAirNodeCallback)
  1950. if add_floor and floorId ~= nil then
  1951. stamp({x=pos.x, y=pos.y - 1, z=pos.z}, pattern, 1, floorId, onAirNodeCallback)
  1952. end
  1953. end
  1954. local addVines = core.humidity >= VINES_REQUIRED_HUMIDITY and core.temperature >= VINES_REQUIRED_TEMPERATURE
  1955. if floorDistance == nil then floorDistance = 0 end
  1956. local distance = round(vector.distance(originp, destp))
  1957. local step = vector.divide(vector.subtract(destp, originp), distance)
  1958. local pos = vector.new(originp)
  1959. local newPos = vector.new(originp)
  1960. excavate(originp, 0 >= floorDistance, false)
  1961. for i = 1, distance do
  1962. newPos.x = newPos.x + step.x
  1963. if round(newPos.x) ~= pos.x then
  1964. pos.x = round(newPos.x)
  1965. excavate(pos, i >= floorDistance, addVines, i <= floorDistance - 1 and i >= floorDistance - 2)
  1966. end
  1967. newPos.y = newPos.y + step.y
  1968. if round(newPos.y) ~= pos.y then
  1969. pos.y = round(newPos.y)
  1970. excavate(pos, i >= floorDistance, addVines, i <= floorDistance - 1 and i >= floorDistance - 2)
  1971. end
  1972. newPos.z = newPos.z + step.z
  1973. if round(newPos.z) ~= pos.z then
  1974. pos.z = round(newPos.z)
  1975. excavate(pos, i >= floorDistance, addVines, i <= floorDistance - 1 and i >= floorDistance - 2)
  1976. end
  1977. end
  1978. -- We only place vines after entire burrow entrance has been carved, to avoid placing
  1979. -- vines on blocks which will later be removed.
  1980. for _,vineFunction in ipairs(vinePlacements) do vineFunction(decoration_list) end
  1981. -- Replace airStandIn with real air.
  1982. -- This two-pass process was neccessary because the vine placing algorithm used
  1983. -- the presense of air to determine if a rock was facing outside and should have a vine.
  1984. -- Single-pass solutions result in vines inside the tunnel (where I'd rather overgrowth spawned)
  1985. for _,stampedIndex in ipairs(stampedIndexes) do
  1986. if data[stampedIndex] == nodeId_airStandIn then
  1987. data[stampedIndex] = nodeId_air
  1988. end
  1989. end
  1990. end
  1991. local function placeNode(x, y, z, node_id)
  1992. if (x >= minp.x and x <= maxp.x and z >= minp.z and z <= maxp.z and y >= minp.y and y <= maxp.y) then
  1993. data[area:index(x, y, z)] = node_id
  1994. end
  1995. end
  1996. local function posInBounds(pos)
  1997. return pos.x >= minp.x and pos.x <= maxp.x and pos.z >= minp.z and pos.z <= maxp.z and pos.y >= minp.y and pos.y <= maxp.y
  1998. end
  1999. local zStart = math_max(core.z - burrowRadius, minp.z)
  2000. local xStart = math_max(core.x - burrowRadius, minp.x)
  2001. local xStop = math_min(core.x + burrowRadius, maxp.x)
  2002. local yStart = math_max(burrowFloor, minp.y)
  2003. -- dig burrow
  2004. local dataBufferIndex = area:index(xStart, yStart, zStart)
  2005. for z = zStart, math_min(core.z + burrowRadius, maxp.z) do
  2006. for x = xStart, xStop do
  2007. local distanceSquared = (x - core.x)*(x - core.x) + (z - core.z)*(z - core.z)
  2008. if distanceSquared < radiusSquared then
  2009. local horz_easing = 1 - distanceSquared / radiusSquared
  2010. for y = math_max(minp.y, burrowFloor + math_floor(1.4 - horz_easing)), math_min(maxp.y, burrowFloor + 1 + math_min(burrowHeight - 1, math_floor(0.8 + burrowHeight * horz_easing))) do
  2011. data[dataBufferIndex + (y - yStart) * area.ystride] = nodeId_air
  2012. end
  2013. end
  2014. dataBufferIndex = dataBufferIndex + 1
  2015. end
  2016. dataBufferIndex = dataBufferIndex + area.zstride - (xStop - xStart + 1)
  2017. end
  2018. local floorId
  2019. if core.biome.node_top == nil then floorId = nil else floorId = minetest.get_content_id(core.biome.node_top) end
  2020. if isAncientBurrow then
  2021. -- island overlaps can only happen at territory edges when a coreType has exclusive=true, so
  2022. -- angle the burrow entrance toward the center of the terrority to avoid any overlapping islands.
  2023. local territoryCenter = vector.new(
  2024. core.type.territorySize * math.floor(core.x / core.type.territorySize) + math.floor(0.5 + core.type.territorySize / 2),
  2025. burrowFloor,
  2026. core.type.territorySize * math.floor(core.z / core.type.territorySize) + math.floor(0.5 + core.type.territorySize / 2)
  2027. )
  2028. local burrowStart = vector.new(core.x, burrowFloor, core.z)
  2029. local direction = vector.direction(burrowStart, territoryCenter)
  2030. local directionOffsetZ = 4
  2031. if direction.z < 0 then directionOffsetZ = -directionOffsetZ end
  2032. burrowStart.z = burrowStart.z + directionOffsetZ -- start the burrow enterance off-center
  2033. burrowStart.x = burrowStart.x + 2 -- start the burrow enterance off-center
  2034. direction = vector.direction(burrowStart, territoryCenter)
  2035. if vector.length(direction) == 0 then direction = vector.direction({x=0, y=0, z=0}, {x=2, y=0, z=1}) end
  2036. local path = vector.add(vector.multiply(direction, core.radius), {x=0, y=-4,z=0})
  2037. local floorStartingFrom = 4 + math.floor(0.5 + core.radius * 0.3)
  2038. -- carve burrow entrance
  2039. local pattern = {{x=0,z=0}, {x=-1,z=0}, {x=1,z=0}, {x=0,z=-1}, {x=0,z=1}}
  2040. carve(burrowStart, vector.add(burrowStart, path), pattern, 2, floorId, floorStartingFrom)
  2041. -- place egg in burrow
  2042. local eggX = core.x
  2043. local eggZ = core.z - directionOffsetZ * 0.75 -- move the egg away from where the burrow entrance is carved
  2044. placeNode(eggX, burrowFloor, eggZ, nodeId_egg)
  2045. if nodeId_gravel ~= nodeId_ignore then placeNode(eggX, burrowFloor - 1, eggZ, nodeId_gravel) end
  2046. if nodeId_cobweb ~= nodeId_ignore then
  2047. placeNode(core.x - 6, burrowFloor + 3, core.z - 1, nodeId_cobweb)
  2048. placeNode(core.x + 4, burrowFloor + 4, core.z + 3, nodeId_cobweb)
  2049. placeNode(core.x + 6, burrowFloor + 1, core.z - 3, nodeId_cobweb)
  2050. end
  2051. else
  2052. -- Only attempt this if it can contain beds and a place to store the diary.
  2053. if (nodeId_bookshelf ~= nodeId_ignore or nodeId_chest ~= nodeId_ignore) and nodeId_bed_top ~= nodeId_ignore and nodeId_bed_bottom ~= nodeId_ignore then
  2054. -- carve stairs to the surface
  2055. local stairsStart = vector.new(core.x - 3, burrowFloor, core.z - 7)
  2056. local stairsbottom = vector.add(stairsStart, {x=0,y=0,z=1})
  2057. local stairsMiddle1 = vector.add(stairsStart, {x=8,y=8,z=0})
  2058. local stairsMiddle2 = vector.add(stairsMiddle1, {x=0,y=0,z=-1})
  2059. local stairsEnd = vector.add(stairsMiddle2, {x=-20,y=20,z=0})
  2060. carve(stairsEnd, stairsMiddle2, {{x=0,z=0}}, 3, floorId, 0)
  2061. carve(stairsMiddle1, stairsStart, {{x=0,z=0}}, 2, floorId, 0)
  2062. local pattern = {{x=0,z=0}, {x=1,z=0}, {x=0,z=2}, {x=0,z=1}, {x=1,z=1}}
  2063. carve(stairsbottom, stairsbottom, pattern, 2, floorId, 0)
  2064. -- fill the outpost
  2065. placeNode(core.x + 2, burrowFloor, core.z + 5, nodeId_bed_top)
  2066. placeNode(core.x + 2, burrowFloor, core.z + 4, nodeId_bed_bottom)
  2067. placeNode(core.x + 2, burrowFloor, core.z + 2, nodeId_bed_top)
  2068. placeNode(core.x + 2, burrowFloor, core.z + 1, nodeId_bed_bottom)
  2069. placeNode(core.x + 4, burrowFloor, core.z + 2, nodeId_bed_top)
  2070. placeNode(core.x + 4, burrowFloor, core.z + 1, nodeId_bed_bottom)
  2071. if (nodeId_torch ~= nodeId_ignore) then
  2072. decoration_list[#decoration_list + 1] = {
  2073. pos={x=core.x, y=burrowFloor + 2, z=core.z + 6},
  2074. node={name = minetest.get_name_from_content_id(nodeId_torch), param2 = 4}
  2075. }
  2076. end
  2077. if nodeId_junk ~= nodeId_ignore then placeNode(core.x - 4, burrowFloor + 1, core.z + 5, nodeId_junk) end
  2078. if nodeId_anvil ~= nodeId_ignore then placeNode(core.x - 6, burrowFloor + 1, core.z, nodeId_anvil) end
  2079. if nodeId_workbench ~= nodeId_ignore then placeNode(core.x - 5, burrowFloor, core.z + 2, nodeId_workbench) end
  2080. if nodeId_cobweb ~= nodeId_ignore then placeNode(core.x + 4, burrowFloor + 4, core.z - 3, nodeId_cobweb) end
  2081. local bookshelf_pos
  2082. local invBookshelf = nil
  2083. local invChest = nil
  2084. if nodeId_chest ~= nodeId_ignore then
  2085. local pos = {x = core.x - 3, y = burrowFloor + 1, z = core.z + 6}
  2086. local nodeName_chest = minetest.get_name_from_content_id(nodeId_chest)
  2087. local nodeNameAtPos = minetest.get_node(pos).name
  2088. -- falls back on the nodeNameAtPos:find("chest") check to avoid a race-condition where if the
  2089. -- chest is opened while nearby areas are being generated, the opened chest may be replaced with
  2090. -- a new empty closed one.
  2091. if nodeNameAtPos ~= nodeName_chest and not nodeNameAtPos:find("chest") then minetest.set_node(pos, {name = nodeName_chest}) end
  2092. if posInBounds(pos) then
  2093. data[area:index(pos.x, pos.y, pos.z)] = nodeId_chest
  2094. invChest = minetest.get_inventory({type = "node", pos = pos})
  2095. end
  2096. end
  2097. if nodeId_bookshelf ~= nodeId_ignore then
  2098. local pos = {x = core.x - 2, y = burrowFloor + 1, z = core.z + 6}
  2099. bookshelf_pos = pos
  2100. if minetest.get_node(pos).name ~= nodeName_bookshelf then minetest.set_node(pos, {name = nodeName_bookshelf}) end
  2101. if posInBounds(pos) then
  2102. data[area:index(pos.x, pos.y, pos.z)] = nodeId_bookshelf
  2103. if not isMineCloneBookshelf then -- mineclone bookshelves are decorational (like Minecraft) and don't contain anything
  2104. invBookshelf = minetest.get_inventory({type = "node", pos = pos})
  2105. end
  2106. end
  2107. end
  2108. if invBookshelf ~= nil or invChest ~= nil then
  2109. -- create diary
  2110. local groundDesc = S("rock")
  2111. if core.biome.node_filler ~= nil then
  2112. local earthNames = string.lower(core.biome.node_filler) .. string.lower(core.biome.node_top)
  2113. if string.match(earthNames, "ice") or string.match(earthNames, "snow") or string.match(earthNames, "frozen") then
  2114. groundDesc = S("ice")
  2115. end
  2116. end
  2117. local book_itemstack = interop.write_book(
  2118. S("Weddell Outpost, November 21"), -- title
  2119. S("Bert Shackleton"), -- owner/author
  2120. S([[The aerostat is lost.
  2121. However, salvage attempts throughout the night managed to
  2122. save most provisions before it finally broke apart and fell.
  2123. ---====---
  2124. This island is highly exposed and the weather did not treat
  2125. the tents well. We have enlarged a sheltered crag in the @1,
  2126. but it is laborous work and the condition of some of the party
  2127. is becoming cause for concern.
  2128. Quite a journey is now required, we cannot stay - nobody will
  2129. look for us here. McNish is attempting to strengthen the gliders.
  2130. ---====---]], groundDesc),
  2131. S("Diary of Bert Shackleton") -- description
  2132. )
  2133. if book_itemstack ~= nil then
  2134. if invBookshelf == nil then
  2135. -- mineclone bookshelves are decorational like Minecraft, put the book in the chest instead
  2136. -- (also testing for nil invBookshelf because it can happen. Weird race condition??)
  2137. if invChest ~= nil then invChest:add_item("main", book_itemstack) end
  2138. else
  2139. -- add the book to the bookshelf and manually trigger update_bookshelf() so its
  2140. -- name will reflect the new contents.
  2141. invBookshelf:add_item("books", book_itemstack)
  2142. local dummyPlayer = {}
  2143. dummyPlayer.get_player_name = function() return "server" end
  2144. minetest.registered_nodes[nodeName_bookshelf].on_metadata_inventory_put(bookshelf_pos, "books", 1, book_itemstack, dummyPlayer)
  2145. end
  2146. end
  2147. end
  2148. if invChest ~= nil then
  2149. -- leave some junk from the expedition in the chest
  2150. local stack
  2151. local function addIfFound(item_aliases, amount)
  2152. for _,name in ipairs(item_aliases) do
  2153. if minetest.registered_items[name] ~= nil then
  2154. stack = ItemStack(name .. " " .. amount)
  2155. invChest:add_item("main", stack)
  2156. break
  2157. end
  2158. end
  2159. end
  2160. addIfFound({"mcl_tools:pick_iron", "default:pick_steel", "main:ironpick"}, 1)
  2161. addIfFound({"binoculars:binoculars"}, 1)
  2162. addIfFound(NODENAMES_WOOD, 10)
  2163. addIfFound({"mcl_torches:torch", "default:torch", "torch:torch"}, 3)
  2164. end
  2165. end
  2166. end
  2167. end
  2168. end
  2169. end
  2170. local function init_secrets()
  2171. nodeId_bed_top = interop.find_node_id({"beds:bed_top", "bed:bed_front"})
  2172. nodeId_bed_bottom = interop.find_node_id({"beds:bed_bottom", "bed:bed_back"})
  2173. nodeId_torch = interop.find_node_id({"mcl_torches:torch_wall", "default:torch_wall", "torch:wall"})
  2174. nodeId_chest = interop.find_node_id({"chest", "mcl_chests:chest", "default:chest", "utility:chest"})
  2175. nodeId_junk = interop.find_node_id({"xdecor:barrel", "cottages:barrel", "homedecor:copper_pans", "vessels:steel_bottle", "mcl_flowerpots:flower_pot"})
  2176. nodeId_anvil = interop.find_node_id({"castle:anvil", "cottages:anvil", "mcl_anvils:anvil", "default:anvil", "main:anvil" }) -- "default:anvil" and "main:anvil" aren't a thing, but perhaps one day.
  2177. nodeId_workbench = interop.find_node_id({"homedecor:table", "xdecor:workbench", "mcl_crafting_table:crafting_table", "default:table", "random_buildings:bench", "craftingtable:craftingtable"}) -- "default:table" isn't a thing, but perhaps one day.
  2178. nodeId_cobweb = interop.find_node_id({"mcl_core:cobweb", "xdecor:cobweb", "homedecor:cobweb_plantlike", "default:cobweb", "main:cobweb"})
  2179. local mineCloneBookshelfName = "mcl_books:bookshelf"
  2180. nodeId_bookshelf = interop.find_node_id({mineCloneBookshelfName, "default:bookshelf"})
  2181. nodeName_bookshelf = minetest.get_name_from_content_id(nodeId_bookshelf)
  2182. isMineCloneBookshelf = nodeName_bookshelf == mineCloneBookshelfName
  2183. if nodeId_cobweb ~= nodeId_ignore then
  2184. -- This game has proper cobwebs, replace any cobwebs this mod may have generated
  2185. -- previously (when a cobweb mod wasn't included) with the proper cobwebs.
  2186. minetest.register_alias(nodeName_standinCobweb, minetest.get_name_from_content_id(nodeId_cobweb))
  2187. elseif minetest.registered_nodes[nodeName_standinCobweb] ~= nil then
  2188. -- use a stand-in cobweb created by this mod
  2189. nodeId_cobweb = minetest.get_content_id(nodeName_standinCobweb)
  2190. end
  2191. end
  2192. ------------------------------------------------------------------------------
  2193. -- End of secrets section
  2194. ------------------------------------------------------------------------------
  2195. local function renderCores(cores, minp, maxp, blockseed)
  2196. local voxelsWereManipulated = false
  2197. local vm, emerge_min, emerge_max = minetest.get_mapgen_object("voxelmanip")
  2198. vm:get_data(data) -- put all nodes except the ground surface in this array
  2199. local area = VoxelArea:new{MinEdge=emerge_min, MaxEdge=emerge_max}
  2200. local overdrawTop = maxp.y + OVERDRAW
  2201. local currentBiomeId = -1
  2202. local nodeId_dust
  2203. local nodeId_top
  2204. local nodeId_filler
  2205. local nodeId_stoneBase
  2206. local nodeId_pondBottom
  2207. local depth_top
  2208. local depth_filler
  2209. local fillerFallsWithGravity
  2210. local floodableDepth
  2211. for z = minp.z, maxp.z do
  2212. local dataBufferIndex = area:index(minp.x, minp.y, z)
  2213. for x = minp.x, maxp.x do
  2214. for _,core in pairs(cores) do
  2215. local coreTop = ALTITUDE + core.y
  2216. local distanceSquared = (x - core.x)*(x - core.x) + (z - core.z)*(z - core.z)
  2217. local radius = core.radius
  2218. local radiusSquared = radius * radius
  2219. if distanceSquared <= radiusSquared then
  2220. -- get the biome details for this core
  2221. if core.biome == nil then setCoreBiomeData(core) end
  2222. if currentBiomeId ~= core.biomeId then
  2223. if core.biome.node_top == nil then nodeId_top = nodeId_stone else nodeId_top = minetest.get_content_id(core.biome.node_top) end
  2224. if core.biome.node_filler == nil then nodeId_filler = nodeId_stone else nodeId_filler = minetest.get_content_id(core.biome.node_filler) end
  2225. if core.biome.node_stone == nil then nodeId_stoneBase = nodeId_stone else nodeId_stoneBase = minetest.get_content_id(core.biome.node_stone) end
  2226. if core.biome.node_dust == nil then nodeId_dust = nodeId_ignore else nodeId_dust = minetest.get_content_id(core.biome.node_dust) end
  2227. if core.biome.node_riverbed == nil then nodeId_pondBottom = nodeId_silt else nodeId_pondBottom = minetest.get_content_id(core.biome.node_riverbed) end
  2228. if core.biome.depth_top == nil then depth_top = 1 else depth_top = core.biome.depth_top end
  2229. if core.biome.depth_filler == nil then depth_filler = 3 else depth_filler = core.biome.depth_filler end
  2230. fillerFallsWithGravity = core.biome.node_filler ~= nil and minetest.registered_items[core.biome.node_filler].groups.falling_node == 1
  2231. --[[Commented out as unnecessary, as a supporting node will be added, but uncommenting
  2232. this will make the strata transition less noisey.
  2233. if fillerFallsWithGravity then
  2234. -- the filler node is affected by gravity and can fall if unsupported, so keep that layer thinner than
  2235. -- core.thickness when possible.
  2236. --depth_filler = math_min(depth_filler, math_max(1, core.thickness - 1))
  2237. end--]]
  2238. floodableDepth = 0
  2239. if nodeId_top ~= nodeId_stone and minetest.registered_items[core.biome.node_top].floodable then
  2240. -- nodeId_top is a node that water floods through, so we can't have ponds appearing at this depth
  2241. floodableDepth = depth_top
  2242. end
  2243. currentBiomeId = core.biomeId
  2244. end
  2245. -- decide on a shape
  2246. local horz_easing
  2247. local noise_weighting = 1
  2248. local shapeType = math_floor(core.depth + radius + core.x) % 5
  2249. if shapeType < 2 then
  2250. -- convex
  2251. -- squared easing function, e = 1 - x²
  2252. horz_easing = 1 - distanceSquared / radiusSquared
  2253. elseif shapeType == 2 then
  2254. -- conical
  2255. -- linear easing function, e = 1 - x
  2256. horz_easing = 1 - math_sqrt(distanceSquared) / radius
  2257. else
  2258. -- concave
  2259. -- root easing function blended/scaled with square easing function,
  2260. -- x = normalised distance from center of core
  2261. -- a = 1 - x²
  2262. -- b = 1 - √x
  2263. -- e = 0.8*a*x + 1.2*b*(1 - x)
  2264. local radiusRoot = core.radiusRoot
  2265. if radiusRoot == nil then
  2266. radiusRoot = math_sqrt(radius)
  2267. core.radiusRoot = radiusRoot
  2268. end
  2269. local squared = 1 - distanceSquared / radiusSquared
  2270. local distance = math_sqrt(distanceSquared)
  2271. local distance_normalized = distance / radius
  2272. local root = 1 - math_sqrt(distance) / radiusRoot
  2273. horz_easing = math_min(1, 0.8*distance_normalized*squared + 1.2*(1-distance_normalized)*root)
  2274. -- this seems to be a more delicate shape that gets wiped out by the
  2275. -- density noise, so lower that
  2276. noise_weighting = 0.63
  2277. end
  2278. if radius + core.depth > 80 then
  2279. -- larger islands shapes have a slower easing transition, which leaves large areas
  2280. -- dominated by the density noise, so reduce the density noise when the island is large.
  2281. -- (the numbers here are arbitrary)
  2282. if radius + core.depth > 120 then
  2283. noise_weighting = 0.35
  2284. else
  2285. noise_weighting = math_min(0.6, noise_weighting)
  2286. end
  2287. end
  2288. local surfaceNoise = noise_surfaceMap:get_2d({x = x, y = z})
  2289. if DEBUG_GEOMETRIC then surfaceNoise = SURFACEMAP_OFFSET end
  2290. local surface = round(surfaceNoise * 3 * (core.thickness + 1) * horz_easing) -- if you change this formular then update maxSufaceRise in on_generated()
  2291. local coreBottom = math_floor(coreTop - (core.thickness + core.depth))
  2292. local noisyDepthOfFiller = depth_filler;
  2293. if noisyDepthOfFiller >= 3 then noisyDepthOfFiller = noisyDepthOfFiller + math_floor(randomNumbers[(x + z) % 256] * 3) - 1 end
  2294. local yBottom = math_max(minp.y, coreBottom - 4) -- the -4 is for rare instances when density noise pushes the bottom of the island deeper
  2295. local yBottomIndex = dataBufferIndex + area.ystride * (yBottom - minp.y) -- equivalent to yBottomIndex = area:index(x, yBottom, z)
  2296. local topBlockIndex = -1
  2297. local bottomBlockIndex = -1
  2298. local vi = yBottomIndex
  2299. local densityNoise = nil
  2300. for y = yBottom, math_min(overdrawTop, coreTop + surface) do
  2301. local vert_easing = math_min(1, (y - coreBottom) / core.depth)
  2302. -- If you change the densityNoise calculation, remember to similarly update the copy of this calculation in the pond code
  2303. densityNoise = noise_density:get3d({x = x, y = y - coreTop, z = z}) -- TODO: Optimize this!!
  2304. densityNoise = noise_weighting * densityNoise + (1 - noise_weighting) * DENSITY_OFFSET
  2305. if DEBUG_GEOMETRIC then densityNoise = DENSITY_OFFSET end
  2306. if densityNoise * ((horz_easing + vert_easing) / 2) >= REQUIRED_DENSITY then
  2307. if vi > topBlockIndex then topBlockIndex = vi end
  2308. if bottomBlockIndex < 0 and y > minp.y then bottomBlockIndex = vi end -- if y==minp.y then we don't know for sure this is the lowest block
  2309. if y > coreTop + surface - depth_top and data[vi] == nodeId_air then
  2310. data[vi] = nodeId_top
  2311. elseif y >= coreTop + surface - (depth_top + noisyDepthOfFiller) then
  2312. data[vi] = nodeId_filler
  2313. else
  2314. data[vi] = nodeId_stoneBase
  2315. end
  2316. end
  2317. vi = vi + area.ystride
  2318. end
  2319. -- ensure nodeId_top blocks also cover the rounded sides of islands (which may be lower
  2320. -- than the flat top), then dust the top surface.
  2321. if topBlockIndex >= 0 then
  2322. voxelsWereManipulated = true;
  2323. -- we either have the highest block, or overdrawTop - but we don't want to set overdrawTop nodes to nodeId_top
  2324. -- (we will err on the side of caution when we can't distinguish the top of a island's side from overdrawTop)
  2325. if overdrawTop >= coreTop + surface or vi > topBlockIndex + area.ystride then
  2326. if topBlockIndex > yBottomIndex and data[topBlockIndex - area.ystride] ~= nodeId_air and data[topBlockIndex + area.ystride] == nodeId_air then
  2327. -- We only set a block to nodeId_top if there's a block under it "holding it up" as
  2328. -- it's better to leave 1-deep noise as stone/whatever.
  2329. data[topBlockIndex] = nodeId_top
  2330. end
  2331. if nodeId_dust ~= nodeId_ignore and data[topBlockIndex + area.ystride] == nodeId_air then
  2332. -- Delay writing dust to the data buffer until after decoration so avoid preventing tree growth etc
  2333. if core.dustLocations == nil then core.dustLocations = {} end
  2334. core.dustLocations[#core.dustLocations + 1] = topBlockIndex + area.ystride
  2335. end
  2336. end
  2337. if fillerFallsWithGravity and bottomBlockIndex >= 0 and data[bottomBlockIndex] == nodeId_filler then
  2338. -- the bottom node is affected by gravity and can fall if unsupported, put some support in
  2339. data[bottomBlockIndex] = nodeId_stoneBase
  2340. end
  2341. end
  2342. -- add ponds of water, trying to make sure they're not on an edge.
  2343. -- (the only time a pond needs to be rendered when densityNoise is nil (i.e. when there was no land at this x, z),
  2344. -- is when the pond is at minp.y - i.e. the reason no land was rendered is it was below minp.y)
  2345. if surfaceNoise < 0 and (densityNoise ~= nil or (coreTop + surface < minp.y and coreTop >= minp.y)) and nodeId_water ~= nodeId_ignore then
  2346. local pondWallBuffer = core.type.pondWallBuffer
  2347. local pondBottom = nodeId_filler
  2348. local pondWater = nodeId_water
  2349. if radius > 18 and core.depth > 15 and nodeId_pondBottom ~= nodeId_ignore then
  2350. -- only give ponds a sandbed when islands are large enough for it not to stick out the side or bottom
  2351. pondBottom = nodeId_pondBottom
  2352. end
  2353. if core.temperature <= ICE_REQUIRED_TEMPERATURE and nodeId_ice ~= nodeId_ignore then pondWater = nodeId_ice end
  2354. if densityNoise == nil then
  2355. -- Rare edge case. If the pond is at minp.y, then no land has been rendered, so
  2356. -- densityNoise hasn't been calculated. Calculate it now.
  2357. densityNoise = noise_density:get3d({x = x, y = minp.y, z = z})
  2358. densityNoise = noise_weighting * densityNoise + (1 - noise_weighting) * DENSITY_OFFSET
  2359. if DEBUG_GEOMETRIC then densityNoise = DENSITY_OFFSET end
  2360. end
  2361. local surfaceDensity = densityNoise * ((horz_easing + 1) / 2)
  2362. local onTheEdge = math_sqrt(distanceSquared) + 1 >= radius
  2363. for y = math_max(minp.y, coreTop + surface), math_min(overdrawTop, coreTop - floodableDepth) do
  2364. if surfaceDensity > REQUIRED_DENSITY then
  2365. vi = dataBufferIndex + area.ystride * (y - minp.y) -- this is the same as vi = area:index(x, y, z)
  2366. if surfaceDensity > (REQUIRED_DENSITY + pondWallBuffer) and not onTheEdge then
  2367. data[vi] = pondWater
  2368. if y > minp.y then data[vi - area.ystride] = pondBottom end
  2369. --remove any dust above ponds
  2370. if core.dustLocations ~= nil and core.dustLocations[#core.dustLocations] == vi + area.ystride then core.dustLocations[#core.dustLocations] = nil end
  2371. else
  2372. -- make sure there are some walls to keep the water in
  2373. if y == coreTop then
  2374. data[vi] = nodeId_top -- to let isIsland() know not to put vines here (only seems to be an issue when pond is 2 deep or more)
  2375. else
  2376. data[vi] = nodeId_filler
  2377. end
  2378. end;
  2379. end
  2380. end
  2381. end;
  2382. end
  2383. end
  2384. dataBufferIndex = dataBufferIndex + 1
  2385. end
  2386. end
  2387. local decorations = {}
  2388. for _,core in ipairs(cores) do
  2389. addDetail_vines(decorations, core, data, area, minp, maxp)
  2390. voxelsWereManipulated = addDetail_skyReef(decorations, core, data, area, minp, maxp) or voxelsWereManipulated
  2391. addDetail_secrets(decorations, core, data, area, minp, maxp)
  2392. end
  2393. if voxelsWereManipulated then
  2394. vm:set_data(data)
  2395. if GENERATE_ORES then minetest.generate_ores(vm) end
  2396. minetest.generate_decorations(vm)
  2397. for _,core in ipairs(cores) do
  2398. addDetail_skyTree(decorations, core, minp, maxp)
  2399. end
  2400. for _,decoration in ipairs(decorations) do
  2401. local nodeAtPos = minetest.get_node(decoration.pos)
  2402. if nodeAtPos.name == "air" or nodeAtPos.name == nodeName_ignore then minetest.set_node(decoration.pos, decoration.node) end
  2403. end
  2404. local dustingInProgress = false
  2405. for _,core in ipairs(cores) do
  2406. if core.dustLocations ~= nil then
  2407. if not dustingInProgress then
  2408. vm:get_data(data)
  2409. dustingInProgress = true
  2410. end
  2411. nodeId_dust = minetest.get_content_id(core.biome.node_dust)
  2412. for _, location in ipairs(core.dustLocations) do
  2413. if data[location] == nodeId_air and data[location - area.ystride] ~= nodeId_air then
  2414. data[location] = nodeId_dust
  2415. end
  2416. end
  2417. end
  2418. end
  2419. if dustingInProgress then vm:set_data(data) end
  2420. -- Lighting is a problem. Two problems really...
  2421. --
  2422. -- Problem 1:
  2423. -- We can't use the usual lua mapgen lighting trick of flags="nolight" e.g.:
  2424. -- minetest.set_mapgen_params({mgname = "singlenode", flags = "nolight"})
  2425. -- (https://forum.minetest.net/viewtopic.php?t=19836)
  2426. --
  2427. -- because the mod is designed to run with other mapgens. So we must set the light
  2428. -- values to zero at islands before calling calc_lighting() to propegate lighting
  2429. -- down from above.
  2430. --
  2431. -- This causes lighting bugs if we zero the whole emerge_min-emerge_max area because
  2432. -- it leaves hard black at the edges of the emerged area (calc_lighting must assume
  2433. -- a value of zero for light outside the region, and be blending that in)
  2434. --
  2435. -- But we can't simply zero only the minp-maxp area instead, because then calc_lighting
  2436. -- reads the daylight values out of the overdraw area and blends those in, cutting
  2437. -- up shadows with lines of daylight along chunk boundaries.
  2438. --
  2439. -- The correct solution is to zero and calculate the whole emerge_min-emerge_max area,
  2440. -- but only write the calculated lighting information from minp-maxp back into the map,
  2441. -- however the API doesn't appear to provide a fast way to do that.
  2442. --
  2443. -- Workaround: zero an area that extends into the overdraw region, but keeps a gap around
  2444. -- the edges to preserve and allow the real light values to propegate in. Then when
  2445. -- calc_lighting is called it will have daylight (or existing values) at the emerge boundary
  2446. -- but not near the chunk boundary. calc_lighting is able to take the edge lighting into
  2447. -- account instead of assuming zero. It's not a perfect solution, but allows shading without
  2448. -- glaringly obvious lighting artifacts, and the minor ill effects should only affect the
  2449. -- islands and be corrected any time lighting is updated.
  2450. --
  2451. --
  2452. -- Problem 2:
  2453. -- We don't want islands to blacken the landscape below them in shadow.
  2454. --
  2455. -- Workaround 1: Instead of zeroing the lighting before propegating from above, set it
  2456. -- to 2, so that shadows are never pitch black. Shadows will still go back to pitch black
  2457. -- though if lighting gets recalculated, e.g. player places a torch then removes it.
  2458. --
  2459. -- Workaround 2: set the bottom of the chunk to full daylight, ensuring that full
  2460. -- daylight is what propegates down below islands. This has the problem of causing a
  2461. -- bright horizontal band of light where islands approach a chunk floor or ceiling,
  2462. -- but Hallelujah Mountains already had that issue due to having propagate_shadow
  2463. -- turned off when calling calc_lighting. This workaround has the same drawback, but
  2464. -- does a much better job of preventing undesired shadows.
  2465. local shadowGap = 1
  2466. local brightMin = {x = emerge_min.x + shadowGap, y = minp.y , z = emerge_min.z + shadowGap}
  2467. local brightMax = {x = emerge_max.x - shadowGap, y = minp.y + 1, z = emerge_max.z - shadowGap}
  2468. local darkMin = {x = emerge_min.x + shadowGap, y = minp.y + 1, z = emerge_min.z + shadowGap}
  2469. local darkMax = {x = emerge_max.x - shadowGap, y = maxp.y , z = emerge_max.z - shadowGap}
  2470. vm:set_lighting({day=2, night=0}, darkMin, darkMax)
  2471. vm:calc_lighting()
  2472. vm:set_lighting({day=15, night=0}, brightMin, brightMax)
  2473. vm:write_to_map() -- seems to be unnecessary when other mods that use vm are running
  2474. for _,core in ipairs(cores) do
  2475. -- place any schematics which should be placed after the landscape
  2476. if addDetail_ancientPortal ~= nil then addDetail_ancientPortal(core) end
  2477. end
  2478. end
  2479. end
  2480. cloudlands.init = function()
  2481. if noise_eddyField == nil then
  2482. init_mapgen()
  2483. init_secrets()
  2484. end
  2485. end
  2486. local function on_generated(minp, maxp, blockseed)
  2487. local memUsageT0
  2488. local osClockT0 = os.clock()
  2489. if DEBUG then memUsageT0 = collectgarbage("count") end
  2490. local largestCoreType = cloudlands.coreTypes[1] -- the first island type is the biggest/thickest
  2491. local maxCoreThickness = largestCoreType.thicknessMax
  2492. local maxCoreDepth = largestCoreType.radiusMax * 3 / 2 -- todo: not sure why this is radius based and not maxDepth based??
  2493. local maxSufaceRise = 3 * (maxCoreThickness + 1)
  2494. if minp.y > ALTITUDE + (ALTITUDE_AMPLITUDE + maxSufaceRise + 10) or -- the 10 is an arbitrary number because sometimes the noise values exceed their normal range.
  2495. maxp.y < ALTITUDE - (ALTITUDE_AMPLITUDE + maxCoreThickness + maxCoreDepth + 10) then
  2496. -- Hallelujah Mountains don't generate here
  2497. return
  2498. end
  2499. cloudlands.init();
  2500. local cores = cloudlands.get_island_details(minp, maxp)
  2501. if DEBUG then
  2502. minetest.log("info", "Cores for on_generated(): " .. #cores)
  2503. for _,core in pairs(cores) do
  2504. minetest.log("core ("..core.x..","..core.y..","..core.z..") r"..core.radius);
  2505. end
  2506. end
  2507. if #cores > 0 then
  2508. -- voxelmanip has mem-leaking issues, avoid creating one if we're not going to need it
  2509. renderCores(cores, minp, maxp, blockseed)
  2510. if DEBUG then
  2511. minetest.log(
  2512. "info",
  2513. MODNAME .. " took "
  2514. .. round((os.clock() - osClockT0) * 1000)
  2515. .. "ms for " .. #cores .. " cores. Uncollected memory delta: "
  2516. .. round(collectgarbage("count") - memUsageT0) .. " KB"
  2517. )
  2518. end
  2519. end
  2520. end
  2521. minetest.register_on_generated(on_generated)
  2522. minetest.register_on_mapgen_init(
  2523. -- invoked after mods initially run but before the environment is created, while the mapgen is being initialized
  2524. function(mgparams)
  2525. worldSeed = mgparams.seed
  2526. --if DEBUG then minetest.set_mapgen_params({mgname = "singlenode"--[[, flags = "nolight"]]}) end
  2527. end
  2528. )