init.lua 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. -- Todo:
  2. -- Allow fortress chunks to extend downward without limit until rock.
  3. fortress = fortress or {}
  4. fortress.modpath = minetest.get_modpath("fortress")
  5. fortress.worldpath = minetest.get_worldpath()
  6. fortress.schempath = fortress.modpath .. "/schems"
  7. fortress.pending = fortress.pending or {}
  8. fortress.active = fortress.active or {}
  9. fortress.dirty = true
  10. dofile(fortress.modpath .. "/default.lua")
  11. local keydirs = {
  12. ["+x"] = {x=1, y=0, z=0},
  13. ["-x"] = {x=-1, y=0, z=0},
  14. ["+y"] = {x=0, y=1, z=0},
  15. ["-y"] = {x=0, y=-1, z=0},
  16. ["+z"] = {x=0, y=0, z=1},
  17. ["-z"] = {x=0, y=0, z=-1},
  18. }
  19. -- Spawn a fortress starting at a position.
  20. -- Public API function. Pass valid values for `pos` and `data`.
  21. function fortress.spawn_fortress(pos, data, start, traversal, build, internal)
  22. -- Build traversal table if not provided. The traversal table allows us to
  23. -- know if a section of fortress was already generated at a cell location. The
  24. -- table contains the hashes of locations where fortress was generated.
  25. if not traversal then traversal = {} end
  26. if not build then build = {} end
  27. if not internal then
  28. internal = {
  29. depth = 0,
  30. time = os.clock(),
  31. limit = {},
  32. pos = {x=pos.x, y=pos.y, z=pos.z},
  33. }
  34. minetest.log("action", "Computing fortress pattern @ " .. minetest.pos_to_string(vector.round(pos)) .. "!")
  35. end
  36. pos = vector.round(pos)
  37. -- Use `initial` if not specified.
  38. -- Multiple initial start-points may be specified, pick a random one.
  39. if not start then
  40. start = data.initial[math.random(1, #data.initial)]
  41. end
  42. -- Chosen chunk must have associated data.
  43. local info = data.chunks[start]
  44. if not info then goto checkdone end
  45. -- Use default offset if none specified.
  46. do
  47. local offset = info.offset or {x=0, y=0, z=0}
  48. pos = vector.round(vector.add(pos, vector.multiply(offset, data.step)))
  49. end
  50. -- Calculate all positions this chunk will potentially occupy.
  51. do
  52. local hash = minetest.hash_node_position(pos)
  53. local hashes = {}
  54. local size = info.size or {x=1, y=1, z=1}
  55. for x = 0, size.x-1, 1 do
  56. for y = 0, size.y-1, 1 do
  57. for z = 0, size.z-1, 1 do
  58. local p3 = vector.round(vector.add(pos, vector.multiply({x=x, y=y, z=z}, data.step)))
  59. local hash = minetest.hash_node_position(p3)
  60. hashes[#hashes+1] = hash
  61. end
  62. end
  63. end
  64. -- Do nothing if this chunk already occupied.
  65. for k, v in ipairs(hashes) do
  66. if traversal[v] then
  67. goto checkdone
  68. end
  69. end
  70. -- Occupy this chunk!
  71. for k, v in ipairs(hashes) do
  72. traversal[v] = true
  73. end
  74. end
  75. -- Obtain relevant parameters for this section of fortress.
  76. -- A chunk may contain multiple schematics to place, each with their own
  77. -- parameters and chance to spawn.
  78. do
  79. -- Chunks may be limited how often they can be used in the fortress pattern.
  80. -- Here, we increment limit if limit is finite (non-zero).
  81. -- Elsewhere in code we read the current limit and reduce chance of
  82. -- chunk being chosen accordingly. Zero means no limit imposed.
  83. local limit = info.limit or 0
  84. internal.limit[start] = internal.limit[start] or 0
  85. if limit > 0 then
  86. internal.limit[start] = internal.limit[start] + 1
  87. end
  88. -- Calculate size of chunk.
  89. local size = vector.multiply(info.size or {x=1, y=1, z=1}, data.step)
  90. -- Add schems which are part of this chunk.
  91. -- A chunk may have multiple schems with different parameters.
  92. local thischunk = info.schem
  93. for k, v in ipairs(thischunk) do
  94. local chance = v.chance or 100
  95. if math.random(1, 100) <= chance then
  96. local file = v.file
  97. local path = fortress.schempath .. "/" .. file .. ".mts"
  98. local adjust = v.adjust or {x=0, y=0, z=0}
  99. local force = true
  100. if type(v.force) == "boolean" then
  101. force = v.force
  102. end
  103. local rotation = v.rotation or "0"
  104. local schempos = vector.add(pos, adjust)
  105. -- Add fortress section to construction queue.
  106. build[#build+1] = {
  107. file = path,
  108. pos = vector.new(schempos),
  109. size = size,
  110. rotation = rotation,
  111. force = force,
  112. }
  113. end
  114. end
  115. end
  116. -- Current chunk may not have next chunks defined.
  117. -- Thus we have reached a dead-end.
  118. if not info.next then
  119. goto checkdone
  120. end
  121. -- Recursively generate further chunks.
  122. for dir, chunks in pairs(info.next) do
  123. local dirvec = keydirs[dir]
  124. local p2 = vector.round(vector.add(vector.multiply(dirvec, data.step), pos))
  125. for index, chunk in ipairs(chunks) do
  126. local info = data.chunks[chunk.chunk]
  127. -- Current chunk must have associated data.
  128. if not info then
  129. goto skipme
  130. end
  131. --minetest.chat_send_all(dump(chunk))
  132. local chance = chunk.chance or 100
  133. local limit = info.limit or 0
  134. -- Adjust chance if chosen chunk is over the limit for this chunk,
  135. -- and the chunk is limited (has a positive, non-zero limit).
  136. if limit > 0 then
  137. local limit2 = internal.limit[chunk.chunk] or 0
  138. if limit2 > limit then
  139. local diff = math.floor(limit2 - limit)
  140. -- Every 1 count past the limit reduces chance by 10.
  141. chance = chance - diff * 10
  142. end
  143. end
  144. -- Add chunk data to fortress pattern if chance test succeeds.
  145. if math.random(1, 100) <= chance then
  146. local continue = false
  147. if type(chunk.continue) == "boolean" then
  148. continue = chunk.continue
  149. end
  150. local p3 = vector.multiply(chunk.shift or {x=0, y=0, z=0}, data.step)
  151. local loc = vector.round(vector.add(p3, p2))
  152. local delay = (math.random(1, 10)/10)+1.0
  153. internal.depth = internal.depth + 1
  154. --minetest.chat_send_all("# Server: Depth " .. internal.depth .. "!")
  155. minetest.after(delay, function()
  156. internal.depth = internal.depth - 1
  157. assert(internal.depth >= 0)
  158. fortress.spawn_fortress(loc, data, chunk.chunk, traversal, build, internal)
  159. end)
  160. -- Generated chunk. Don't need to continue through chunks for this dir.
  161. if not continue then
  162. break
  163. end
  164. end
  165. ::skipme::
  166. end
  167. end
  168. -- Check if all build-data is gathered yet.
  169. ::checkdone::
  170. if internal.depth == 0 then
  171. minetest.log("action", "Finished generating fortress pattern in " .. math.floor(os.clock()-internal.time) .. " seconds!")
  172. -- Push build data to pending queue.
  173. for k, v in ipairs(build) do
  174. (fortress.pending)[#(fortress.pending)+1] = v
  175. end
  176. fortress.dirty = true
  177. -- Save data for later, perhaps after a restart.
  178. -- But more commonly, fortress will be generated this session.
  179. fortress.save_data()
  180. end
  181. end
  182. -- Using data in the pending table, construct a fortress.
  183. -- This data is created by attempting to spawn a fortress,
  184. -- and is saved on shutdown to allow resuming after restart.
  185. -- This function is called once a second.
  186. function fortress.resume_construction()
  187. if #fortress.pending == 0 then
  188. -- Nothing to write!
  189. return
  190. end
  191. -- Count entries in active list (starts out contigious, may have holes).
  192. local count = 0
  193. for k, v in pairs(fortress.active) do
  194. count = count + 1
  195. end
  196. if count > 0 then
  197. -- Write already in progress!
  198. return
  199. end
  200. -- Swap buffers.
  201. fortress.active = fortress.pending
  202. fortress.pending = {}
  203. fortress.dirty = true
  204. local internal = {
  205. depth = 0,
  206. time = os.clock(),
  207. }
  208. minetest.log("action", "Generating fortress structure!")
  209. -- The first time we iterate over the active list it is a contigious
  210. -- array. During the process that this starts, the array gains holes.
  211. local timer = 1
  212. for k, v in pairs(fortress.active) do
  213. internal.depth = internal.depth + 1
  214. minetest.after(timer, function()
  215. -- Positions to preload. Need a larger area in order to make sure any protections are discovered.
  216. local minp = vector.add(v.pos, vector.new(-16, -16, -16))
  217. local maxp = vector.add(v.pos, vector.add(v.size, vector.new(16, 16, 16)))
  218. local tbparam = {}
  219. -- Callback function. Will be called when area is emerged.
  220. local cb = function(blockpos, action, calls_remaining, param)
  221. -- We don't do anything until the last callback.
  222. if calls_remaining ~= 0 then
  223. -- Don't decrement the depth counter.
  224. -- We haven't actually done anything.
  225. goto checkdone
  226. end
  227. if action == core.EMERGE_CANCELLED or action == core.EMERGE_ERRORED then
  228. minetest.log("error", "Failed to emerge area for fortress chunk at " .. minetest.pos_to_string(v.pos) .. "!")
  229. -- Don't decrement the depth counter.
  230. -- We haven't actually done anything.
  231. goto checkdone
  232. end
  233. internal.depth = internal.depth - 1
  234. if fortress.is_protected(minp, maxp) then
  235. minetest.log("error", "Cannot place fortress chunk at " .. minetest.pos_to_string(v.pos) .. " due to protection!")
  236. fortress.active[k] = nil
  237. fortress.dirty = true
  238. goto checkdone
  239. end
  240. minetest.place_schematic(v.pos, v.file, v.rotation, {}, v.force)
  241. fortress.active[k] = nil
  242. fortress.dirty = true
  243. minetest.log("action", "Placed fortress section @ " .. minetest.pos_to_string(v.pos) .. "!")
  244. ::checkdone::
  245. if internal.depth == 0 then
  246. minetest.log("action", "Fortress fully generated in " .. math.floor(os.clock()-internal.time) .. " seconds!")
  247. fortress.save_data()
  248. end
  249. end
  250. -- The fortress chunk is placed after generating map.
  251. minetest.emerge_area(minp, maxp, cb, tbparam)
  252. end)
  253. -- Separate calls to build fortress sections by random time, sequentially.
  254. timer = timer + (math.random(1, 100)/20)
  255. end
  256. end
  257. -- Quickly check for protection in an area.
  258. function fortress.is_protected(minp, maxp)
  259. -- Step size, to avoid checking every single node.
  260. -- This assumes protections cannot be smaller than this size.
  261. local ss = 5
  262. local check = minetest.test_protection
  263. for x=minp.x, maxp.x, ss do
  264. for y=minp.y, maxp.y, ss do
  265. for z=minp.z, maxp.z, ss do
  266. if check({x=x, y=y, z=z}, "") then
  267. -- Protections are present.
  268. return true
  269. end
  270. end
  271. end
  272. end
  273. -- Nothing in the area is protected.
  274. return false
  275. end
  276. function fortress.save_data()
  277. if not fortress.dirty then
  278. return
  279. end
  280. local data = {}
  281. for k, v in ipairs(fortress.pending) do
  282. data[#data+1] = v
  283. end
  284. for k, v in pairs(fortress.active) do
  285. data[#data+1] = v
  286. end
  287. local str = minetest.serialize(data)
  288. if type(str) ~= "string" then
  289. return
  290. end
  291. local file = io.open(fortress.worldpath .. "/fortress.dat", "w")
  292. if file then
  293. file:write(str)
  294. file:close()
  295. minetest.log("action", "Saved " .. #data .. " pending fortress sections!")
  296. fortress.dirty = false
  297. end
  298. end
  299. function fortress.load_data()
  300. local file = io.open(fortress.worldpath .. "/fortress.dat", "r")
  301. if file then
  302. -- Data should always be a contigious array.
  303. local data = minetest.deserialize(file:read("*all"))
  304. if type(data) == "table" then
  305. fortress.pending = data
  306. fortress.dirty = false
  307. end
  308. file:close()
  309. minetest.log("action", "Loaded " .. #data .. " pending fortress sections!")
  310. end
  311. end
  312. function fortress.chat_command(name, param)
  313. local player = minetest.get_player_by_name(name)
  314. if not player or not player:is_player() then
  315. return
  316. end
  317. local pos = vector.round(player:get_pos())
  318. fortress.spawn_fortress(pos, fortress.default)
  319. end
  320. if not fortress.run_once then
  321. fortress.load_data()
  322. local delay = 1
  323. local timer = 0
  324. minetest.register_globalstep(function(dtime)
  325. timer = timer + dtime
  326. if timer < delay then
  327. return
  328. end
  329. timer = 0
  330. return fortress.resume_construction()
  331. end)
  332. minetest.register_on_shutdown(function()
  333. return fortress.save_data()
  334. end)
  335. minetest.register_on_mapsave(function()
  336. return fortress.save_data()
  337. end)
  338. minetest.register_chatcommand("spawn_fortress", {
  339. params = "",
  340. description = "Spawn a fortress starting at your current location.",
  341. privs = {server=true},
  342. func = function(...)
  343. fortress.chat_command(...)
  344. return true
  345. end,
  346. })
  347. local c = "fortress:core"
  348. local f = fortress.modpath .. "/init.lua"
  349. reload.register_file(c, f, false)
  350. fortress.run_once = true
  351. end