portal_examples.lua 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. --[[
  2. Nether mod portal examples for Minetest
  3. These portal API examples work independently of the Nether realm
  4. and Nether portal. To try these examples, enable them in:
  5. Minetest -> Settings -> All settings -> Mods -> nether
  6. Once enabled, details on how to build them can be found in dungeon
  7. chests in the book of portals.
  8. --
  9. Copyright (C) 2020 Treer
  10. Permission to use, copy, modify, and/or distribute this software for
  11. any purpose with or without fee is hereby granted, provided that the
  12. above copyright notice and this permission notice appear in all copies.
  13. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
  14. WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
  15. WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR
  16. BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES
  17. OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
  18. WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
  19. ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
  20. SOFTWARE.
  21. ]]--
  22. local S = nether.get_translator
  23. local ENABLE_PORTAL_EXAMPLE_FLOATLANDS = false
  24. local ENABLE_PORTAL_EXAMPLE_SURFACETRAVEL = false
  25. -- Sets how far a Surface Portal will travel, measured in cells along the Moore curve,
  26. -- which are about 117 nodes square each. Larger numbers will generally mean further distance
  27. -- as-the-crow-flies, but this will not always be true due to the how the Moore curve
  28. -- frequently doubles back upon itself.
  29. -- This doubling-back prevents the surface portal from taking players easily accross the
  30. -- map - the curve is 262144 cells long!
  31. local SURFACE_TRAVEL_DISTANCE = 26
  32. --=================================================--
  33. -- Portal to the Floatlands, playable code example --
  34. --==================================================--
  35. local FLOATLANDS_ENABLED
  36. local FLOATLAND_LEVEL = 1280
  37. if minetest.settings:get_bool("nether_enable_portal_example_floatlands", ENABLE_PORTAL_EXAMPLE_FLOATLANDS) or ENABLE_PORTAL_EXAMPLE_FLOATLANDS then
  38. local floatlands_flavortext = ""
  39. if minetest.get_mapgen_setting("mg_name") == "v7" then
  40. local mgv7_spflags = minetest.get_mapgen_setting("mgv7_spflags")
  41. FLOATLANDS_ENABLED = mgv7_spflags ~= nil and mgv7_spflags:find("floatlands") ~= nil and mgv7_spflags:find("nofloatlands") == nil
  42. FLOATLAND_LEVEL = minetest.get_mapgen_setting("mgv7_floatland_level") or 1280
  43. if FLOATLANDS_ENABLED then
  44. floatlands_flavortext = "\n\n " .. S("There is a floating land of hills and forests up there, over the edges of which is a perilous drop all the way back down to sea level. We have not found how far these pristine lands extend. I have half a mind to retire there one day.")
  45. end
  46. end
  47. nether.register_portal("floatlands_portal", {
  48. shape = nether.PortalShape_Platform,
  49. frame_node_name = "default:ice",
  50. wormhole_node_color = 7, -- 7 is white
  51. particle_texture = {
  52. name = "nether_particle_anim1.png",
  53. animation = {
  54. type = "vertical_frames",
  55. aspect_w = 7,
  56. aspect_h = 7,
  57. length = 1,
  58. },
  59. scale = 1.5
  60. },
  61. title = S("Floatlands Portal"),
  62. book_of_portals_pagetext = S([[Requiring 21 blocks of ice, and constructed in the shape of a 3 × 3 platform with walls, or like a bowl. A finished platform is 2 blocks high, and 5 blocks wide at the widest in both directions.
  63. This portal is different to the others, rather than acting akin to a doorway it appears to the eye more like a small pool of water which can be stepped into. Upon setting foot in the portal we found ourselves at a tremendous altitude.@1]],
  64. floatlands_flavortext),
  65. is_within_realm = function(pos) -- return true if pos is inside the Nether
  66. return pos.y > FLOATLAND_LEVEL - 200
  67. end,
  68. find_realm_anchorPos = function(surface_anchorPos, player_name)
  69. -- TODO: Once paramat finishes adjusting the floatlands, implement a surface algorithm that finds land
  70. local destination_pos = {x = surface_anchorPos.x ,y = FLOATLAND_LEVEL + 2, z = surface_anchorPos.z}
  71. -- a y_factor of 0 makes the search ignore the altitude of the portals (as long as they are in the Floatlands)
  72. local existing_portal_location, existing_portal_orientation = nether.find_nearest_working_portal("floatlands_portal", destination_pos, 10, 0)
  73. if existing_portal_location ~= nil then
  74. return existing_portal_location, existing_portal_orientation
  75. else
  76. return destination_pos
  77. end
  78. end
  79. })
  80. end
  81. --==============================================--
  82. -- Surface-travel portal, playable code example --
  83. --==============================================--
  84. -- These Moore Curve functions required by surface_portal's find_surface_anchorPos() will
  85. -- be assigned later in this file.
  86. local get_moore_distance -- will be function get_moore_distance(cell_count, x, y): integer
  87. local get_moore_coords -- will be function get_moore_coords(cell_count, distance): pos2d
  88. if minetest.settings:get_bool("nether_enable_portal_example_surfacetravel", ENABLE_PORTAL_EXAMPLE_SURFACETRAVEL) or ENABLE_PORTAL_EXAMPLE_SURFACETRAVEL then
  89. nether.register_portal("surface_portal", {
  90. shape = nether.PortalShape_Circular,
  91. frame_node_name = "default:tinblock",
  92. wormhole_node_name = "nether:portal_alt",
  93. wormhole_node_color = 4, -- 4 is cyan
  94. title = S("Surface Portal"),
  95. book_of_portals_pagetext = S([[Requiring 16 blocks of tin and constructed in a circular fashion, a finished frame is seven blocks wide, seven blocks high, and stands vertically like a doorway.
  96. These travel a distance along the ground, and even when constructed deep underground will link back up to the surface. They appear to favor a strange direction, with the exit portal linking back only for as long as the portal stays open — attempting to reopen a portal from the exit doorway leads to a new destination along this favored direction. It has stymied our ability to study the behavior of these portals because without constructing dual portals and keeping both open it's hard to step through more than one and still be able to return home.
  97. Due to such difficulties, we never learned what determines the direction and distance where the matching twin portal will appear, and I have lost my friend and protégé. In cavalier youth and with little more than a rucksack, Coudreau has decided to follow the chain as far as it goes, and has not been seen since. Coudreau believes it works in epicycles, but I am not convinced. Still, I cling to the hope that one day the portal will open and Coudreau will step out from whichever place leads to this one, perhaps with an epic tale to tell.]]),
  98. is_within_realm = function(pos)
  99. -- Always return true, because these portals always just take you around the surface
  100. -- rather than taking you to a different realm
  101. return true
  102. end,
  103. find_realm_anchorPos = function(surface_anchorPos, player_name)
  104. -- This function isn't needed, since this type of portal always goes to the surface
  105. minetest.log("error" , "find_realm_anchorPos called for surface portal")
  106. return {x=0, y=0, z=0}
  107. end,
  108. find_surface_anchorPos = function(realm_anchorPos, player_name)
  109. -- A portal definition doesn't normally need to provide a find_surface_anchorPos() function,
  110. -- since find_surface_target_y() will be used by default, but these portals travel around the
  111. -- surface (following a Moore curve) so will be calculating a different x and z to realm_anchorPos.
  112. local cellCount = 512
  113. local maxDistFromOrigin = 30000 -- the world edges are at X=30927, X=−30912, Z=30927 and Z=−30912
  114. -- clip realm_anchorPos to maxDistFromOrigin, and move the origin so that all values are positive
  115. local x = math.min(maxDistFromOrigin, math.max(-maxDistFromOrigin, realm_anchorPos.x)) + maxDistFromOrigin
  116. local z = math.min(maxDistFromOrigin, math.max(-maxDistFromOrigin, realm_anchorPos.z)) + maxDistFromOrigin
  117. local divisor = math.ceil(maxDistFromOrigin * 2 / cellCount)
  118. local distance = get_moore_distance(cellCount, math.floor(x / divisor + 0.5), math.floor(z / divisor + 0.5))
  119. local destination_distance = (distance + SURFACE_TRAVEL_DISTANCE) % (cellCount * cellCount)
  120. local moore_pos = get_moore_coords(cellCount, destination_distance)
  121. local target_x = moore_pos.x * divisor - maxDistFromOrigin
  122. local target_z = moore_pos.y * divisor - maxDistFromOrigin
  123. local search_radius = divisor / 2 - 5 -- any portal within this area will do
  124. -- a y_factor of 0 makes the search ignore the altitude of the portals
  125. local existing_portal_location, existing_portal_orientation =
  126. nether.find_nearest_working_portal("surface_portal", {x = target_x, y = 0, z = target_z}, search_radius, 0)
  127. if existing_portal_location ~= nil then
  128. -- use the existing portal that was found near target_x, target_z
  129. return existing_portal_location, existing_portal_orientation
  130. else
  131. -- find a good location for the new portal, or if that isn't possible then at
  132. -- least adjust the coords a little so portals don't line up in a grid
  133. local adj_x, adj_z = 0, 0
  134. -- Deterministically look for a location in the cell where get_spawn_level() can give
  135. -- us a surface height, since nether.find_surface_target_y() works *much* better when
  136. -- it can use get_spawn_level()
  137. local prng = PcgRandom( -- seed the prng so that all portals for these Moore Curve coords will use the same random location
  138. moore_pos.x * 65732 +
  139. moore_pos.y * 729 +
  140. minetest.get_mapgen_setting("seed") * 3
  141. )
  142. local attemptLimit = 15 -- how many attempts we'll make at finding a good location
  143. for attempt = 1, attemptLimit do
  144. adj_x = math.floor(prng:rand_normal_dist(-search_radius, search_radius, 2) + 0.5)
  145. adj_z = math.floor(prng:rand_normal_dist(-search_radius, search_radius, 2) + 0.5)
  146. if minetest.get_spawn_level == nil or minetest.get_spawn_level(target_x + adj_x, target_z + adj_z) ~= nil then
  147. -- Found a location which will be at ground level - unless a player has built there.
  148. -- Or this is MT 0.4 which does not have get_spawn_level(), so there's no point looking
  149. -- at any further further random locations.
  150. break
  151. end
  152. end
  153. local destination_pos = {x = target_x + adj_x, y = 0, z = target_z + adj_z}
  154. destination_pos.y = nether.find_surface_target_y(destination_pos.x, destination_pos.z, "surface_portal", player_name)
  155. return destination_pos
  156. end
  157. end
  158. })
  159. end
  160. --=========================================--
  161. -- Hilbert curve and Moore curve functions --
  162. --=========================================--
  163. -- These are space-filling curves, used by the surface_portal example as a way to determine where
  164. -- to place portals. https://en.wikipedia.org/wiki/Moore_curve
  165. -- Flip a quadrant on a diagonal axis
  166. -- cell_count is the number of cells across the square is split into, and must be a power of 2
  167. -- if flip_twice is true then pos does not change (even numbers of flips cancel out)
  168. -- if flip_direction is true then the position is flipped along the \ diagonal
  169. -- if flip_direction is false then the position is flipped along the / diagonal
  170. local function hilbert_flip(cell_count, pos, flip_direction, flip_twice)
  171. if not flip_twice then
  172. if flip_direction then
  173. pos.x = (cell_count - 1) - pos.x;
  174. pos.y = (cell_count - 1) - pos.y;
  175. end
  176. local temp_x = pos.x;
  177. pos.x = pos.y;
  178. pos.y = temp_x;
  179. end
  180. end
  181. local function test_bit(cell_count, value, flag)
  182. local bit_value = cell_count / 2
  183. while bit_value > flag and bit_value >= 1 do
  184. if value >= bit_value then value = value - bit_value end
  185. bit_value = bit_value / 2
  186. end
  187. return value >= bit_value
  188. end
  189. -- Converts (x,y) to distance
  190. -- starts at bottom left corner, i.e. (0, 0)
  191. -- ends at bottom right corner, i.e. (cell_count - 1, 0)
  192. local function get_hilbert_distance (cell_count, x, y)
  193. local distance = 0
  194. local pos = {x=x, y=y}
  195. local rx, ry
  196. local s = cell_count / 2
  197. while s > 0 do
  198. if test_bit(cell_count, pos.x, s) then rx = 1 else rx = 0 end
  199. if test_bit(cell_count, pos.y, s) then ry = 1 else ry = 0 end
  200. local rx_XOR_ry = rx
  201. if ry == 1 then rx_XOR_ry = 1 - rx_XOR_ry end -- XOR'd ry against rx
  202. distance = distance + s * s * (2 * rx + rx_XOR_ry)
  203. hilbert_flip(cell_count, pos, rx > 0, ry > 0);
  204. s = math.floor(s / 2)
  205. end
  206. return distance;
  207. end
  208. -- Converts distance to (x,y)
  209. local function get_hilbert_coords(cell_count, distance)
  210. local pos = {x=0, y=0}
  211. local rx, ry
  212. local s = 1
  213. while s < cell_count do
  214. rx = math.floor(distance / 2) % 2
  215. ry = distance % 2
  216. if rx == 1 then ry = 1 - ry end -- XOR ry with rx
  217. hilbert_flip(s, pos, rx > 0, ry > 0);
  218. pos.x = pos.x + s * rx
  219. pos.y = pos.y + s * ry
  220. distance = math.floor(distance / 4)
  221. s = s * 2
  222. end
  223. return pos
  224. end
  225. -- Converts (x,y) to distance
  226. -- A Moore curve is a variation of the Hilbert curve that has the start and
  227. -- end next to each other.
  228. -- Top middle point is the start/end location
  229. get_moore_distance = function(cell_count, x, y)
  230. local quadLength = cell_count / 2
  231. local quadrant = 1 - math.floor(y / quadLength)
  232. if math.floor(x / quadLength) == 1 then quadrant = 3 - quadrant end
  233. local flipDirection = x < quadLength
  234. local pos = {x = x % quadLength, y = y % quadLength}
  235. hilbert_flip(quadLength, pos, flipDirection, false)
  236. return (quadrant * quadLength * quadLength) + get_hilbert_distance(quadLength, pos.x, pos.y)
  237. end
  238. -- Converts distance to (x,y)
  239. -- A Moore curve is a variation of the Hilbert curve that has the start and
  240. -- end next to each other.
  241. -- Top middle point is the start/end location
  242. get_moore_coords = function(cell_count, distance)
  243. local quadLength = cell_count / 2
  244. local quadDistance = quadLength * quadLength
  245. local quadrant = math.floor(distance / quadDistance)
  246. local flipDirection = distance * 2 < cell_count * cell_count
  247. local pos = get_hilbert_coords(quadLength, distance % quadDistance)
  248. hilbert_flip(quadLength, pos, flipDirection, false)
  249. if quadrant >= 2 then pos.x = pos.x + quadLength end
  250. if quadrant % 3 == 0 then pos.y = pos.y + quadLength end
  251. return pos
  252. end