portal_api.lua 100 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305
  1. --[[
  2. Portal API for Minetest
  3. See portal_api.txt for documentation
  4. --
  5. Copyright (C) 2020 Treer
  6. Permission to use, copy, modify, and/or distribute this software for
  7. any purpose with or without fee is hereby granted, provided that the
  8. above copyright notice and this permission notice appear in all copies.
  9. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
  10. WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
  11. WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR
  12. BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES
  13. OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
  14. WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
  15. ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
  16. SOFTWARE.
  17. ]]--
  18. local DEBUG = false
  19. local DEBUG_IGNORE_MODSTORAGE = false -- setting true prevents portals from knowing where other portals are, forcing find_realm_anchorpos() etc. to be executed every time
  20. nether.registered_portals = {}
  21. nether.registered_portals_count = 0
  22. -- Exposes a list of node names that are used as frame nodes by registered portals
  23. nether.is_frame_node = {}
  24. -- gives the colour values in nether_portals_palette.png that are used by the wormhole colorfacedir
  25. -- hardware colouring.
  26. nether.portals_palette = {
  27. [0] = {r = 128, g = 0, b = 128, asString = "#800080"}, -- traditional/magenta
  28. [1] = {r = 0, g = 0, b = 0, asString = "#000000"}, -- black
  29. [2] = {r = 19, g = 19, b = 255, asString = "#1313FF"}, -- blue
  30. [3] = {r = 55, g = 168, b = 0, asString = "#37A800"}, -- green
  31. [4] = {r = 141, g = 237, b = 255, asString = "#8DEDFF"}, -- cyan
  32. [5] = {r = 221, g = 0, b = 0, asString = "#DD0000"}, -- red
  33. [6] = {r = 255, g = 240, b = 0, asString = "#FFF000"}, -- yellow
  34. [7] = {r = 255, g = 255, b = 255, asString = "#FFFFFF"} -- white
  35. }
  36. if minetest.get_mod_storage == nil then
  37. error(nether.modname .. " does not support Minetest versions earlier than 0.4.16", 0)
  38. end
  39. --[[
  40. Positions
  41. =========
  42. p1 & p2 p1 and p2 is the system used by earlier versions of the nether mod, which the portal_api
  43. is forwards and backwards compatible with.
  44. p1 is the bottom/west/south corner of the portal, and p2 is the opposite corner, together
  45. they define the bounding volume for the portal.
  46. The value of p1 and p2 is kept in the metadata of every node in the portal
  47. WormholePos The location of the node that a portal's target is set to, and a player is teleported
  48. to. It can also be used to test whether a portal is active.
  49. AnchorPos Introduced by the portal_api. Coordinates for portals are normally given in terms of
  50. the AnchorPos. The AnchorPos does not change with portal orientation - portals rotate
  51. around the AnchorPos. Ideally an AnchorPos would be near the bottom center of a portal
  52. shape, but this is not the case with PortalShape_Traditional to keep comptaibility with
  53. earlier versions of the nether mod.
  54. Usually an orientation is required with an AnchorPos.
  55. Orientation is yaw, either 0 or 90, 0 meaning a portal that faces north/south - i.e. obsidian
  56. running east/west.
  57. TimerPos The portal_api replaces ABMs with a single node timer per portal, and the TimerPos is the
  58. node in which that timer is located. Extra metadata is also kept in the TimerPos node.
  59. Portal shapes
  60. =============
  61. For the PortalShape_Traditional implementation, p1, p2, anchorPos, wormholdPos and TimerPos are defined
  62. as follows:
  63. .
  64. +--------+--------+--------+--------+
  65. | | Frame | |
  66. | | | | p2 |
  67. +--------+--------+--------+--------+
  68. | | | |
  69. | | | |
  70. +--------+ + +--------+
  71. | | Wormhole | |
  72. | | | |
  73. +--------+ + +--------+
  74. | |Wormhole | |
  75. | | Pos | |
  76. +--------+--------+--------+--------+
  77. AnchorPos|TimerPos| | |
  78. | p1 | | | |
  79. +--------+--------+--------+--------+
  80. +X/East or +Z/North ----->
  81. A better location for AnchorPos would be directly under WormholePos, as it's more centered
  82. and you don't need to know the portal's orientation to find AnchorPos from the WormholePos
  83. or vice-versa, however AnchorPos is in the bottom/south/west-corner to keep compatibility
  84. with earlier versions of nether mod (which only records portal corners p1 & p2 in the node
  85. metadata).
  86. ]]
  87. local facedir_up, facedir_north, facedir_south, facedir_east, facedir_west, facedir_down = 0, 4, 8, 12, 16, 20
  88. local __ = {name = "air", prob = 0}
  89. local AA = {name = "air", prob = 255, force_place = true}
  90. local ON = {name = "default:obsidian", facedir = facedir_north + 0, prob = 255, force_place = true}
  91. local ON2 = {name = "default:obsidian", facedir = facedir_north + 1, prob = 255, force_place = true}
  92. local ON3 = {name = "default:obsidian", facedir = facedir_north + 2, prob = 255, force_place = true}
  93. local ON4 = {name = "default:obsidian", facedir = facedir_north + 3, prob = 255, force_place = true}
  94. local OS = {name = "default:obsidian", facedir = facedir_south, prob = 255, force_place = true}
  95. local OE = {name = "default:obsidian", facedir = facedir_east, prob = 255, force_place = true}
  96. local OW = {name = "default:obsidian", facedir = facedir_west, prob = 255, force_place = true}
  97. local OU = {name = "default:obsidian", facedir = facedir_up + 0, prob = 255, force_place = true}
  98. local OU2 = {name = "default:obsidian", facedir = facedir_up + 1, prob = 255, force_place = true}
  99. local OU3 = {name = "default:obsidian", facedir = facedir_up + 2, prob = 255, force_place = true}
  100. local OU4 = {name = "default:obsidian", facedir = facedir_up + 3, prob = 255, force_place = true}
  101. local OD = {name = "default:obsidian", facedir = facedir_down, prob = 255, force_place = true}
  102. -- facedirNodeList is a list of node references which should have their facedir value copied into
  103. -- param2 before placing a schematic. The facedir values will only be copied when the portal's frame
  104. -- node has a paramtype2 of "facedir" or "colorfacedir".
  105. -- Having schematics provide this list avoids needing to check every node in the schematic volume.
  106. local facedirNodeList = {ON, ON2, ON3, ON4, OS, OE, OW, OU, OU2, OU3, OU4, OD}
  107. -- This object defines a portal's shape, segregating the shape logic code from portal behaviour code.
  108. -- You can create a new "PortalShape" definition object which implements the same
  109. -- functions if you wish to register a custom shaped portal in register_portal(). Examples of other
  110. -- shapes follow after PortalShape_Traditional.
  111. -- Since it's symmetric, this PortalShape definition has only implemented orientations of 0 and 90
  112. nether.PortalShape_Traditional = {
  113. name = "Traditional",
  114. size = vector.new(4, 5, 1), -- size of the portal, and not necessarily the size of the schematic,
  115. -- which may clear area around the portal.
  116. is_horizontal = false, -- whether the wormhole is a vertical or horizontal surface
  117. diagram_image = {
  118. image = "nether_book_diagram_traditional.png", -- The diagram to be shown in the Book of Portals
  119. width = 142,
  120. height = 305
  121. },
  122. -- returns the coords for minetest.place_schematic() that will place the schematic on the anchorPos
  123. get_schematicPos_from_anchorPos = function(anchorPos, orientation)
  124. assert(orientation, "no orientation passed")
  125. if orientation == 0 then
  126. return {x = anchorPos.x, y = anchorPos.y, z = anchorPos.z - 2}
  127. else
  128. return {x = anchorPos.x - 2, y = anchorPos.y, z = anchorPos.z }
  129. end
  130. end,
  131. get_wormholePos_from_anchorPos = function(anchorPos, orientation)
  132. assert(orientation, "no orientation passed")
  133. if orientation == 0 then
  134. return {x = anchorPos.x + 1, y = anchorPos.y + 1, z = anchorPos.z }
  135. else
  136. return {x = anchorPos.x, y = anchorPos.y + 1, z = anchorPos.z + 1}
  137. end
  138. end,
  139. get_anchorPos_from_wormholePos = function(wormholePos, orientation)
  140. assert(orientation, "no orientation passed")
  141. if orientation == 0 then
  142. return {x = wormholePos.x - 1, y = wormholePos.y - 1, z = wormholePos.z }
  143. else
  144. return {x = wormholePos.x, y = wormholePos.y - 1, z = wormholePos.z - 1}
  145. end
  146. end,
  147. -- p1 and p2 are used to keep maps compatible with earlier versions of this mod.
  148. -- p1 is the bottom/west/south corner of the portal, and p2 is the opposite corner, together
  149. -- they define the bounding volume for the portal.
  150. get_p1_and_p2_from_anchorPos = function(self, anchorPos, orientation)
  151. assert(orientation, "no orientation passed")
  152. assert(self ~= nil and self.name == nether.PortalShape_Traditional.name, "Must pass self as first argument, or use shape:func() instead of shape.func()")
  153. local p1 = anchorPos -- PortalShape_Traditional puts the anchorPos at p1 for backwards&forwards compatibility
  154. local p2
  155. if orientation == 0 then
  156. p2 = {x = p1.x + self.size.x - 1, y = p1.y + self.size.y - 1, z = p1.z }
  157. else
  158. p2 = {x = p1.x, y = p1.y + self.size.y - 1, z = p1.z + self.size.x - 1}
  159. end
  160. return p1, p2
  161. end,
  162. get_anchorPos_and_orientation_from_p1_and_p2 = function(p1, p2)
  163. if p1.z == p2.z then
  164. return p1, 0
  165. elseif p1.x == p2.x then
  166. return p1, 90
  167. else
  168. -- this KISS implementation will break you've made a 3D PortalShape definition
  169. minetest.log("error", "get_anchorPos_and_orientation_from_p1_and_p2 failed on p1=" .. minetest.pos_to_string(p1) .. " p2=" .. minetest.pos_to_string(p2))
  170. end
  171. end,
  172. -- returns true if function was applied to all frame nodes
  173. apply_func_to_frame_nodes = function(anchorPos, orientation, func)
  174. -- a 4x5 portal is small enough that hardcoded positions is simpler that procedural code
  175. local shortCircuited
  176. if orientation == 0 then
  177. -- use short-circuiting of boolean evaluation to allow func() to cause an abort by returning true
  178. shortCircuited =
  179. func({x = anchorPos.x + 0, y = anchorPos.y, z = anchorPos.z}) or
  180. func({x = anchorPos.x + 1, y = anchorPos.y, z = anchorPos.z}) or
  181. func({x = anchorPos.x + 2, y = anchorPos.y, z = anchorPos.z}) or
  182. func({x = anchorPos.x + 3, y = anchorPos.y, z = anchorPos.z}) or
  183. func({x = anchorPos.x + 0, y = anchorPos.y + 4, z = anchorPos.z}) or
  184. func({x = anchorPos.x + 1, y = anchorPos.y + 4, z = anchorPos.z}) or
  185. func({x = anchorPos.x + 2, y = anchorPos.y + 4, z = anchorPos.z}) or
  186. func({x = anchorPos.x + 3, y = anchorPos.y + 4, z = anchorPos.z}) or
  187. func({x = anchorPos.x, y = anchorPos.y + 1, z = anchorPos.z}) or
  188. func({x = anchorPos.x, y = anchorPos.y + 2, z = anchorPos.z}) or
  189. func({x = anchorPos.x, y = anchorPos.y + 3, z = anchorPos.z}) or
  190. func({x = anchorPos.x + 3, y = anchorPos.y + 1, z = anchorPos.z}) or
  191. func({x = anchorPos.x + 3, y = anchorPos.y + 2, z = anchorPos.z}) or
  192. func({x = anchorPos.x + 3, y = anchorPos.y + 3, z = anchorPos.z})
  193. else
  194. shortCircuited =
  195. func({x = anchorPos.x, y = anchorPos.y, z = anchorPos.z + 0}) or
  196. func({x = anchorPos.x, y = anchorPos.y, z = anchorPos.z + 1}) or
  197. func({x = anchorPos.x, y = anchorPos.y, z = anchorPos.z + 2}) or
  198. func({x = anchorPos.x, y = anchorPos.y, z = anchorPos.z + 3}) or
  199. func({x = anchorPos.x, y = anchorPos.y + 4, z = anchorPos.z + 0}) or
  200. func({x = anchorPos.x, y = anchorPos.y + 4, z = anchorPos.z + 1}) or
  201. func({x = anchorPos.x, y = anchorPos.y + 4, z = anchorPos.z + 2}) or
  202. func({x = anchorPos.x, y = anchorPos.y + 4, z = anchorPos.z + 3}) or
  203. func({x = anchorPos.x, y = anchorPos.y + 1, z = anchorPos.z }) or
  204. func({x = anchorPos.x, y = anchorPos.y + 2, z = anchorPos.z }) or
  205. func({x = anchorPos.x, y = anchorPos.y + 3, z = anchorPos.z }) or
  206. func({x = anchorPos.x, y = anchorPos.y + 1, z = anchorPos.z + 3}) or
  207. func({x = anchorPos.x, y = anchorPos.y + 2, z = anchorPos.z + 3}) or
  208. func({x = anchorPos.x, y = anchorPos.y + 3, z = anchorPos.z + 3})
  209. end
  210. return not shortCircuited
  211. end,
  212. -- returns true if function was applied to all wormhole nodes
  213. apply_func_to_wormhole_nodes = function(anchorPos, orientation, func)
  214. local shortCircuited
  215. if orientation == 0 then
  216. local wormholePos = {x = anchorPos.x + 1, y = anchorPos.y + 1, z = anchorPos.z}
  217. -- use short-circuiting of boolean evaluation to allow func() to cause an abort by returning true
  218. shortCircuited =
  219. func({x = wormholePos.x + 0, y = wormholePos.y + 0, z = wormholePos.z}) or
  220. func({x = wormholePos.x + 1, y = wormholePos.y + 0, z = wormholePos.z}) or
  221. func({x = wormholePos.x + 0, y = wormholePos.y + 1, z = wormholePos.z}) or
  222. func({x = wormholePos.x + 1, y = wormholePos.y + 1, z = wormholePos.z}) or
  223. func({x = wormholePos.x + 0, y = wormholePos.y + 2, z = wormholePos.z}) or
  224. func({x = wormholePos.x + 1, y = wormholePos.y + 2, z = wormholePos.z})
  225. else
  226. local wormholePos = {x = anchorPos.x, y = anchorPos.y + 1, z = anchorPos.z + 1}
  227. shortCircuited =
  228. func({x = wormholePos.x, y = wormholePos.y + 0, z = wormholePos.z + 0}) or
  229. func({x = wormholePos.x, y = wormholePos.y + 0, z = wormholePos.z + 1}) or
  230. func({x = wormholePos.x, y = wormholePos.y + 1, z = wormholePos.z + 0}) or
  231. func({x = wormholePos.x, y = wormholePos.y + 1, z = wormholePos.z + 1}) or
  232. func({x = wormholePos.x, y = wormholePos.y + 2, z = wormholePos.z + 0}) or
  233. func({x = wormholePos.x, y = wormholePos.y + 2, z = wormholePos.z + 1})
  234. end
  235. return not shortCircuited
  236. end,
  237. -- Check for whether the portal is blocked in, and if so then provide a safe way
  238. -- on one side for the player to step out of the portal. Suggest including a roof
  239. -- incase the portal was blocked with lava flowing from above.
  240. -- If portal can appear in mid-air then can also check for that and add a platform.
  241. disable_portal_trap = function(anchorPos, orientation)
  242. assert(orientation, "no orientation passed")
  243. -- Not implemented yet. It may not need to be implemented because if you
  244. -- wait in a portal long enough you teleport again. So a trap portal would have to link
  245. -- to one of two blocked-in portals which link to each other - which is possible, but
  246. -- quite extreme.
  247. end,
  248. schematic = {
  249. size = {x = 4, y = 5, z = 5},
  250. data = { -- note that data is upside down
  251. __,__,__,__,
  252. AA,AA,AA,AA,
  253. AA,AA,AA,AA,
  254. AA,AA,AA,AA,
  255. AA,AA,AA,AA,
  256. __,__,__,__,
  257. AA,AA,AA,AA,
  258. AA,AA,AA,AA,
  259. AA,AA,AA,AA,
  260. AA,AA,AA,AA,
  261. ON,OW,OE,ON2,
  262. OU,AA,AA,OU,
  263. OU,AA,AA,OU,
  264. OU,AA,AA,OU,
  265. ON4,OE,OW,ON3,
  266. __,__,__,__,
  267. AA,AA,AA,AA,
  268. AA,AA,AA,AA,
  269. AA,AA,AA,AA,
  270. AA,AA,AA,AA,
  271. __,__,__,__,
  272. AA,AA,AA,AA,
  273. AA,AA,AA,AA,
  274. AA,AA,AA,AA,
  275. AA,AA,AA,AA,
  276. },
  277. facedirNodes = facedirNodeList
  278. }
  279. } -- End of PortalShape_Traditional class
  280. -- Example alternative PortalShape
  281. nether.PortalShape_Circular = {
  282. name = "Circular",
  283. size = vector.new(7, 7, 1), -- size of the portal, and not necessarily the size of the schematic,
  284. -- which may clear area around the portal.
  285. is_horizontal = false, -- whether the wormhole is a vertical or horizontal surface
  286. diagram_image = {
  287. image = "nether_book_diagram_circular.png", -- The diagram to be shown in the Book of Portals
  288. width = 149,
  289. height = 243
  290. },
  291. -- returns the coords for minetest.place_schematic() that will place the schematic on the anchorPos
  292. get_schematicPos_from_anchorPos = function(anchorPos, orientation)
  293. assert(orientation, "no orientation passed")
  294. if orientation == 0 then
  295. return {x = anchorPos.x - 3, y = anchorPos.y, z = anchorPos.z - 3}
  296. else
  297. return {x = anchorPos.x - 3, y = anchorPos.y, z = anchorPos.z - 3 }
  298. end
  299. end,
  300. get_wormholePos_from_anchorPos = function(anchorPos, orientation)
  301. -- wormholePos is the node above anchorPos
  302. return {x = anchorPos.x, y = anchorPos.y + 1, z = anchorPos.z}
  303. end,
  304. get_anchorPos_from_wormholePos = function(wormholePos, orientation)
  305. -- wormholePos is the node above anchorPos
  306. return {x = wormholePos.x, y = wormholePos.y - 1, z = wormholePos.z}
  307. end,
  308. -- p1 and p2 are used to keep maps compatible with earlier versions of this mod.
  309. -- p1 is the bottom/west/south corner of the portal, and p2 is the opposite corner, together
  310. -- they define the bounding volume for the portal.
  311. get_p1_and_p2_from_anchorPos = function(self, anchorPos, orientation)
  312. assert(orientation, "no orientation passed")
  313. assert(self ~= nil and self.name == nether.PortalShape_Circular.name, "Must pass self as first argument, or use shape:func() instead of shape.func()")
  314. local p1
  315. local p2
  316. if orientation == 0 then
  317. p1 = {x = anchorPos.x - 3, y = anchorPos.y, z = anchorPos.z }
  318. p2 = {x = p1.x + self.size.x - 1, y = p1.y + self.size.y - 1, z = p1.z }
  319. else
  320. p1 = {x = anchorPos.x, y = anchorPos.y, z = anchorPos.z - 3 }
  321. p2 = {x = p1.x, y = p1.y + self.size.y - 1, z = p1.z + self.size.x - 1}
  322. end
  323. return p1, p2
  324. end,
  325. get_anchorPos_and_orientation_from_p1_and_p2 = function(p1, p2)
  326. if p1.z == p2.z then
  327. return {x= p1.x + 3, y = p1.y, z = p1.z }, 0
  328. elseif p1.x == p2.x then
  329. return {x= p1.x, y = p1.y, z = p1.z + 3}, 90
  330. end
  331. end,
  332. apply_func_to_frame_nodes = function(anchorPos, orientation, func)
  333. local shortCircuited
  334. if orientation == 0 then
  335. -- use short-circuiting of boolean evaluation to allow func() to cause an abort by returning true
  336. shortCircuited =
  337. func({x = anchorPos.x + 0, y = anchorPos.y + 0, z = anchorPos.z}) or
  338. func({x = anchorPos.x + 1, y = anchorPos.y + 0, z = anchorPos.z}) or func({x = anchorPos.x - 1, y = anchorPos.y + 0, z = anchorPos.z}) or
  339. func({x = anchorPos.x + 2, y = anchorPos.y + 1, z = anchorPos.z}) or func({x = anchorPos.x - 2, y = anchorPos.y + 1, z = anchorPos.z}) or
  340. func({x = anchorPos.x + 3, y = anchorPos.y + 2, z = anchorPos.z}) or func({x = anchorPos.x - 3, y = anchorPos.y + 2, z = anchorPos.z}) or
  341. func({x = anchorPos.x + 3, y = anchorPos.y + 3, z = anchorPos.z}) or func({x = anchorPos.x - 3, y = anchorPos.y + 3, z = anchorPos.z}) or
  342. func({x = anchorPos.x + 3, y = anchorPos.y + 4, z = anchorPos.z}) or func({x = anchorPos.x - 3, y = anchorPos.y + 4, z = anchorPos.z}) or
  343. func({x = anchorPos.x + 2, y = anchorPos.y + 5, z = anchorPos.z}) or func({x = anchorPos.x - 2, y = anchorPos.y + 5, z = anchorPos.z}) or
  344. func({x = anchorPos.x + 1, y = anchorPos.y + 6, z = anchorPos.z}) or func({x = anchorPos.x - 1, y = anchorPos.y + 6, z = anchorPos.z}) or
  345. func({x = anchorPos.x + 0, y = anchorPos.y + 6, z = anchorPos.z})
  346. else
  347. shortCircuited =
  348. func({x = anchorPos.x, y = anchorPos.y + 0, z = anchorPos.z + 0}) or
  349. func({x = anchorPos.x, y = anchorPos.y + 0, z = anchorPos.z + 1}) or func({x = anchorPos.x, y = anchorPos.y + 0, z = anchorPos.z - 1}) or
  350. func({x = anchorPos.x, y = anchorPos.y + 1, z = anchorPos.z + 2}) or func({x = anchorPos.x, y = anchorPos.y + 1, z = anchorPos.z - 2}) or
  351. func({x = anchorPos.x, y = anchorPos.y + 2, z = anchorPos.z + 3}) or func({x = anchorPos.x, y = anchorPos.y + 2, z = anchorPos.z - 3}) or
  352. func({x = anchorPos.x, y = anchorPos.y + 3, z = anchorPos.z + 3}) or func({x = anchorPos.x, y = anchorPos.y + 3, z = anchorPos.z - 3}) or
  353. func({x = anchorPos.x, y = anchorPos.y + 4, z = anchorPos.z + 3}) or func({x = anchorPos.x, y = anchorPos.y + 4, z = anchorPos.z - 3}) or
  354. func({x = anchorPos.x, y = anchorPos.y + 5, z = anchorPos.z + 2}) or func({x = anchorPos.x, y = anchorPos.y + 5, z = anchorPos.z - 2}) or
  355. func({x = anchorPos.x, y = anchorPos.y + 6, z = anchorPos.z + 1}) or func({x = anchorPos.x, y = anchorPos.y + 6, z = anchorPos.z - 1}) or
  356. func({x = anchorPos.x, y = anchorPos.y + 6, z = anchorPos.z + 0})
  357. end
  358. return not shortCircuited
  359. end,
  360. -- returns true if function was applied to all wormhole nodes
  361. apply_func_to_wormhole_nodes = function(anchorPos, orientation, func)
  362. local xRange = 2
  363. local zRange = 0
  364. if orientation ~= 0 then
  365. xRange = 0
  366. zRange = 2
  367. end
  368. local xEdge, yEdge, zEdge
  369. local pos = {}
  370. for x = -xRange, xRange do
  371. pos.x = anchorPos.x + x
  372. xEdge = x == -xRange or x == xRange
  373. for z = -zRange, zRange do
  374. zEdge = z == -zRange or z == zRange
  375. pos.z = anchorPos.z + z
  376. for y = 1, 5 do
  377. yEdge = y == 1 or y == 5
  378. if not (yEdge and xEdge and zEdge) then
  379. pos.y = anchorPos.y + y
  380. if func(pos) then
  381. -- func() caused an abort by returning true
  382. return false
  383. end
  384. end
  385. end
  386. end
  387. end
  388. return true
  389. end,
  390. -- Check for whether the portal is blocked in, and if so then provide a safe way
  391. -- on one side for the player to step out of the portal. Suggest including a roof
  392. -- incase the portal was blocked with lava flowing from above.
  393. -- If portal can appear in mid-air then can also check for that and add a platform.
  394. disable_portal_trap = function(anchorPos, orientation)
  395. assert(orientation, "no orientation passed")
  396. -- Not implemented.
  397. end,
  398. schematic = {
  399. size = {x = 7, y = 7, z = 7},
  400. data = { -- note that data is upside down
  401. __,__,__,__,__,__,__,
  402. __,__,__,__,__,__,__,
  403. __,__,AA,AA,AA,__,__,
  404. __,__,AA,AA,AA,__,__,
  405. __,__,AA,AA,AA,__,__,
  406. __,__,__,__,__,__,__,
  407. __,__,__,__,__,__,__,
  408. __,__,__,__,__,__,__,
  409. __,AA,AA,AA,AA,AA,__,
  410. __,AA,AA,AA,AA,AA,__,
  411. __,AA,AA,AA,AA,AA,__,
  412. __,AA,AA,AA,AA,AA,__,
  413. __,AA,AA,AA,AA,AA,__,
  414. __,__,__,__,__,__,__,
  415. __,__,__,__,__,__,__,
  416. __,AA,AA,AA,AA,AA,__,
  417. AA,AA,AA,AA,AA,AA,AA,
  418. AA,AA,AA,AA,AA,AA,AA,
  419. AA,AA,AA,AA,AA,AA,AA,
  420. __,AA,AA,AA,AA,AA,__,
  421. __,__,AA,AA,AA,__,__,
  422. __,__,OW,OW,OW,__,__,
  423. __,ON,AA,AA,AA,ON2,__,
  424. OU,AA,AA,AA,AA,AA,OD,
  425. OU,AA,AA,AA,AA,AA,OD,
  426. OU,AA,AA,AA,AA,AA,OD,
  427. __,ON4,AA,AA,AA,ON3,__,
  428. __,__,OE,OE,OE,__,__,
  429. __,__,__,__,__,__,__,
  430. __,AA,AA,AA,AA,AA,__,
  431. AA,AA,AA,AA,AA,AA,AA,
  432. AA,AA,AA,AA,AA,AA,AA,
  433. AA,AA,AA,AA,AA,AA,AA,
  434. __,AA,AA,AA,AA,AA,__,
  435. __,__,AA,AA,AA,__,__,
  436. __,__,__,__,__,__,__,
  437. __,AA,AA,AA,AA,AA,__,
  438. __,AA,AA,AA,AA,AA,__,
  439. __,AA,AA,AA,AA,AA,__,
  440. __,AA,AA,AA,AA,AA,__,
  441. __,AA,AA,AA,AA,AA,__,
  442. __,__,__,__,__,__,__,
  443. __,__,__,__,__,__,__,
  444. __,__,__,__,__,__,__,
  445. __,__,AA,AA,AA,__,__,
  446. __,__,AA,AA,AA,__,__,
  447. __,__,AA,AA,AA,__,__,
  448. __,__,__,__,__,__,__,
  449. __,__,__,__,__,__,__,
  450. },
  451. facedirNodes = facedirNodeList
  452. }
  453. } -- End of PortalShape_Circular class
  454. -- Example alternative PortalShape
  455. -- This platform shape is symmetrical around the y-axis, so the orientation value never matters.
  456. nether.PortalShape_Platform = {
  457. name = "Platform",
  458. size = vector.new(5, 2, 5), -- size of the portal, and not necessarily the size of the schematic,
  459. -- which may clear area around the portal.
  460. is_horizontal = true, -- whether the wormhole is a vertical or horizontal surface
  461. diagram_image = {
  462. image = "nether_book_diagram_platform.png", -- The diagram to be shown in the Book of Portals
  463. width = 200,
  464. height = 130
  465. },
  466. -- returns the coords for minetest.place_schematic() that will place the schematic on the anchorPos
  467. get_schematicPos_from_anchorPos = function(anchorPos, orientation)
  468. return {x = anchorPos.x - 2, y = anchorPos.y, z = anchorPos.z - 2}
  469. end,
  470. get_wormholePos_from_anchorPos = function(anchorPos, orientation)
  471. -- wormholePos is the node above anchorPos
  472. return {x = anchorPos.x, y = anchorPos.y + 1, z = anchorPos.z}
  473. end,
  474. get_anchorPos_from_wormholePos = function(wormholePos, orientation)
  475. -- wormholePos is the node above anchorPos
  476. return {x = wormholePos.x, y = wormholePos.y - 1, z = wormholePos.z}
  477. end,
  478. -- p1 and p2 are used to keep maps compatible with earlier versions of this mod.
  479. -- p1 is the bottom/west/south corner of the portal, and p2 is the opposite corner, together
  480. -- they define the bounding volume for the portal.
  481. get_p1_and_p2_from_anchorPos = function(self, anchorPos, orientation)
  482. assert(self ~= nil and self.name == nether.PortalShape_Platform.name, "Must pass self as first argument, or use shape:func() instead of shape.func()")
  483. local p1 = {x = anchorPos.x - 2, y = anchorPos.y, z = anchorPos.z - 2}
  484. local p2 = {x = anchorPos.x + 2, y = anchorPos.y + 1, z = anchorPos.z + 2}
  485. return p1, p2
  486. end,
  487. get_anchorPos_and_orientation_from_p1_and_p2 = function(p1, p2)
  488. return {x= p1.x + 2, y = p1.y, z = p1.z + 2}, 0
  489. end,
  490. apply_func_to_frame_nodes = function(anchorPos, orientation, func)
  491. local shortCircuited
  492. local yPlus1 = anchorPos.y + 1
  493. -- use short-circuiting of boolean evaluation to allow func() to cause an abort by returning true
  494. shortCircuited =
  495. func({x = anchorPos.x - 2, y = yPlus1, z = anchorPos.z - 1}) or func({x = anchorPos.x + 2, y = yPlus1, z = anchorPos.z - 1}) or
  496. func({x = anchorPos.x - 2, y = yPlus1, z = anchorPos.z }) or func({x = anchorPos.x + 2, y = yPlus1, z = anchorPos.z }) or
  497. func({x = anchorPos.x - 2, y = yPlus1, z = anchorPos.z + 1}) or func({x = anchorPos.x + 2, y = yPlus1, z = anchorPos.z + 1}) or
  498. func({x = anchorPos.x - 1, y = yPlus1, z = anchorPos.z - 2}) or func({x = anchorPos.x - 1, y = yPlus1, z = anchorPos.z + 2}) or
  499. func({x = anchorPos.x , y = yPlus1, z = anchorPos.z - 2}) or func({x = anchorPos.x , y = yPlus1, z = anchorPos.z + 2}) or
  500. func({x = anchorPos.x + 1, y = yPlus1, z = anchorPos.z - 2}) or func({x = anchorPos.x + 1, y = yPlus1, z = anchorPos.z + 2}) or
  501. func({x = anchorPos.x - 1, y = anchorPos.y, z = anchorPos.z - 1}) or
  502. func({x = anchorPos.x - 1, y = anchorPos.y, z = anchorPos.z }) or
  503. func({x = anchorPos.x - 1, y = anchorPos.y, z = anchorPos.z + 1}) or
  504. func({x = anchorPos.x , y = anchorPos.y, z = anchorPos.z - 1}) or
  505. func({x = anchorPos.x , y = anchorPos.y, z = anchorPos.z }) or
  506. func({x = anchorPos.x , y = anchorPos.y, z = anchorPos.z + 1}) or
  507. func({x = anchorPos.x + 1, y = anchorPos.y, z = anchorPos.z - 1}) or
  508. func({x = anchorPos.x + 1, y = anchorPos.y, z = anchorPos.z }) or
  509. func({x = anchorPos.x + 1, y = anchorPos.y, z = anchorPos.z + 1})
  510. return not shortCircuited
  511. end,
  512. -- returns true if function was applied to all wormhole nodes
  513. apply_func_to_wormhole_nodes = function(anchorPos, orientation, func)
  514. local shortCircuited
  515. local yPlus1 = anchorPos.y + 1
  516. -- use short-circuiting of boolean evaluation to allow func() to cause an abort by returning true
  517. shortCircuited =
  518. func({x = anchorPos.x - 1, y = yPlus1, z = anchorPos.z - 1}) or
  519. func({x = anchorPos.x - 1, y = yPlus1, z = anchorPos.z }) or
  520. func({x = anchorPos.x - 1, y = yPlus1, z = anchorPos.z + 1}) or
  521. func({x = anchorPos.x , y = yPlus1, z = anchorPos.z - 1}) or
  522. func({x = anchorPos.x , y = yPlus1, z = anchorPos.z }) or
  523. func({x = anchorPos.x , y = yPlus1, z = anchorPos.z + 1}) or
  524. func({x = anchorPos.x + 1, y = yPlus1, z = anchorPos.z - 1}) or
  525. func({x = anchorPos.x + 1, y = yPlus1, z = anchorPos.z }) or
  526. func({x = anchorPos.x + 1, y = yPlus1, z = anchorPos.z + 1})
  527. return not shortCircuited
  528. end,
  529. -- Check for suffocation
  530. disable_portal_trap = function(anchorPos, orientation)
  531. -- Not implemented.
  532. end,
  533. schematic = {
  534. size = {x = 5, y = 5, z = 5},
  535. data = { -- note that data is upside down
  536. __,__,__,__,__,
  537. OU4,OW,OW,OW,OU3,
  538. __,AA,AA,AA,__,
  539. __,AA,AA,AA,__,
  540. __,__,__,__,__,
  541. __,OU4,OW,OU3,__,
  542. ON,AA,AA,AA,OS,
  543. AA,AA,AA,AA,AA,
  544. AA,AA,AA,AA,AA,
  545. __,AA,AA,AA,__,
  546. __,ON,OD,OS,__,
  547. ON,AA,AA,AA,OS,
  548. AA,AA,AA,AA,AA,
  549. AA,AA,AA,AA,AA,
  550. __,AA,AA,AA,__,
  551. __,OU,OE,OU2,__,
  552. ON,AA,AA,AA,OS,
  553. AA,AA,AA,AA,AA,
  554. AA,AA,AA,AA,AA,
  555. __,AA,AA,AA,__,
  556. __,__,__,__,__,
  557. OU,OE,OE,OE,OU2,
  558. __,AA,AA,AA,__,
  559. __,AA,AA,AA,__,
  560. __,__,__,__,__,
  561. },
  562. facedirNodes = facedirNodeList
  563. }
  564. } -- End of PortalShape_Platform class
  565. --====================================================--
  566. --======== End of PortalShape implementations ========--
  567. --====================================================--
  568. -- Portal implementation functions --
  569. -- =============================== --
  570. local ignition_item_name
  571. local S = nether.get_translator
  572. local mod_storage = minetest.get_mod_storage()
  573. local meseconsAvailable = minetest.get_modpath("mesecon") ~= nil and minetest.global_exists("mesecon")
  574. local book_added_as_treasure = false
  575. local function get_timerPos_from_p1_and_p2(p1, p2)
  576. -- Pick a frame node for the portal's timer.
  577. --
  578. -- The timer event will need to know the portal definition, which can be determined by
  579. -- what the portal frame is made from, so the timer node should be on the frame.
  580. -- The timer event will also need to know its portal orientation, but unless someone
  581. -- makes a cubic portal shape, orientation can be determined from p1 and p2 in the node's
  582. -- metadata (frame nodes don't have orientation set in param2 like wormhole nodes do).
  583. --
  584. -- We shouldn't pick p1 or p2 as it's possible for two orthogonal portals to share
  585. -- the same p1, etc. - or at least it was - there's code to try to stop that now.
  586. --
  587. -- I'll pick the bottom center node of the portal, since that works for rectangular portals
  588. -- and if someone want to make a circular portal then that positon will still likely be part
  589. -- of the frame.
  590. return {
  591. x = math.floor((p1.x + p2.x) / 2),
  592. y = p1.y,
  593. z = math.floor((p1.z + p2.z) / 2),
  594. }
  595. end
  596. -- orientation is the yaw rotation degrees passed to place_schematic: 0, 90, 180, or 270
  597. -- color is a value from 0 to 7 corresponding to the color of pixels in nether_portals_palette.png
  598. -- portal_is_horizontal is a bool indicating whether the portal lies flat or stands vertically
  599. local function get_colorfacedir_from_color_and_orientation(color, orientation, portal_is_horizontal)
  600. assert(orientation, "no orientation passed")
  601. local axis_direction, rotation
  602. local dir = math.floor((orientation % 360) / 90 + 0.5)
  603. -- if the portal is vertical then node axis direction will be +Y (up) and portal orientation
  604. -- will set the node's rotation.
  605. -- if the portal is horizontal then the node axis direction reflects the yaw orientation and
  606. -- the node's rotation will be whatever's needed to keep the texture horizontal (either 0 or 1)
  607. if portal_is_horizontal then
  608. if dir == 0 then axis_direction = 1 end -- North
  609. if dir == 1 then axis_direction = 3 end -- East
  610. if dir == 2 then axis_direction = 2 end -- South
  611. if dir == 3 then axis_direction = 4 end -- West
  612. rotation = math.floor(axis_direction / 2); -- a rotation is only needed if axis_direction is east or west
  613. else
  614. axis_direction = 0 -- 0 is up, or +Y
  615. rotation = dir
  616. end
  617. -- wormhole nodes have a paramtype2 of colorfacedir, which means the
  618. -- high 3 bits are palette, followed by 3 direction bits and 2 rotation bits.
  619. -- We set the palette bits and rotation
  620. return rotation + axis_direction * 4 + color * 32
  621. end
  622. local function get_orientation_from_colorfacedir(param2)
  623. local axis_direction = 0
  624. -- Strip off the top 6 bits to leave the 2 rotation bits, unfortunately MT lua has no bitwise '&'
  625. -- (high 3 bits are palette, followed by 3 direction bits then 2 rotation bits)
  626. if param2 >= 128 then param2 = param2 - 128 end
  627. if param2 >= 64 then param2 = param2 - 64 end
  628. if param2 >= 32 then param2 = param2 - 32 end
  629. if param2 >= 16 then param2 = param2 - 16; axis_direction = axis_direction + 4 end
  630. if param2 >= 8 then param2 = param2 - 8; axis_direction = axis_direction + 2 end
  631. if param2 >= 4 then param2 = param2 - 4; axis_direction = axis_direction + 1 end
  632. -- if the portal is vertical then node axis direction will be +Y (up) and portal orientation
  633. -- will set the node's rotation.
  634. -- if the portal is horizontal then the node axis direction reflects the yaw orientation and
  635. -- the node's rotation will be whatever's needed to keep the texture horizontal (either 0 or 1)
  636. if axis_direction == 0 or axis_direction == 5 then
  637. -- portal is vertical
  638. return param2 * 90
  639. else
  640. if axis_direction == 1 then return 0 end
  641. if axis_direction == 3 then return 90 end
  642. if axis_direction == 2 then return 180 end
  643. if axis_direction == 4 then return 270 end
  644. end
  645. end
  646. -- We want wormhole nodes to only emit mesecon energy orthogonally to the
  647. -- wormhole surface so that the wormhole will not send power to the frame,
  648. -- this allows the portal frame to listen for mesecon energy from external switches/wires etc.
  649. function get_mesecon_emission_rules_from_colorfacedir(param2)
  650. local axis_direction = 0
  651. -- Strip off the top 6 bits to leave the 2 rotation bits, unfortunately MT lua has no bitwise '&'
  652. -- (high 3 bits are palette, followed by 3 direction bits then 2 rotation bits)
  653. if param2 >= 128 then param2 = param2 - 128 end
  654. if param2 >= 64 then param2 = param2 - 64 end
  655. if param2 >= 32 then param2 = param2 - 32 end
  656. if param2 >= 16 then param2 = param2 - 16; axis_direction = axis_direction + 4 end
  657. if param2 >= 8 then param2 = param2 - 8; axis_direction = axis_direction + 2 end
  658. if param2 >= 4 then param2 = param2 - 4; axis_direction = axis_direction + 1 end
  659. -- if the portal is vertical then node axis_direction will be +Y (up) and node rotation
  660. -- will reflect the portal's yaw orientation.
  661. -- If the portal is horizontal then the node axis direction reflects the yaw orientation and
  662. -- the node's rotation will be whatever's needed to keep the texture horizontal (either 0 or 1)
  663. local rules
  664. if axis_direction == 0 or axis_direction == 5 then
  665. -- portal is vertical
  666. rules = {{x = 0, y = 0, z = 1}, {x = 0, y = 0, z = -1}}
  667. if param2 % 2 ~= 0 then
  668. rules = mesecon.rotate_rules_right(rules)
  669. end
  670. else
  671. -- portal is horizontal, only emit up
  672. rules = {{x = 0, y = 1, z = 0}}
  673. end
  674. return rules
  675. end
  676. nether.get_mesecon_emission_rules_from_colorfacedir = get_mesecon_emission_rules_from_colorfacedir -- make the function available to nodes.lua
  677. -- Combining frame_node_name, p1, and p2 will always be enough to uniquely identify a portal_definition
  678. -- WITHOUT needing to inspect the world. register_portal() will enforce this.
  679. -- This function does not require the portal to be in a loaded chunk.
  680. -- Returns nil if no portal_definition matches the arguments
  681. local function get_portal_definition(frame_node_name, p1, p2)
  682. local size = vector.add(vector.subtract(p2, p1), 1)
  683. local rotated_size = {x = size.z, y = size.y, z = size.x}
  684. for _, portal_def in pairs(nether.registered_portals) do
  685. if portal_def.frame_node_name == frame_node_name then
  686. if vector.equals(size, portal_def.shape.size) or vector.equals(rotated_size, portal_def.shape.size) then
  687. return portal_def
  688. end
  689. end
  690. end
  691. return nil
  692. end
  693. -- Returns a list of all portal_definitions with a frame made of frame_node_name.
  694. -- Ideally no two portal types will be built from the same frame material so this call might be enough
  695. -- to uniquely identify a portal_definition without needing to inspect the world, HOWEVER we shouldn't
  696. -- cramp anyone's style and prohibit non-nether use of obsidian to make portals, so it returns a list.
  697. -- If the list contains more than one item then routines like ignite_portal() will have to search twice
  698. -- for a portal and take twice the CPU.
  699. local function list_portal_definitions_for_frame_node(frame_node_name)
  700. local result = {}
  701. for _, portal_def in pairs(nether.registered_portals) do
  702. if portal_def.frame_node_name == frame_node_name then table.insert(result, portal_def) end
  703. end
  704. return result
  705. end
  706. -- Add portal information to mod storage, so new portals may find existing portals near the target location.
  707. -- Do this whenever a portal is created or changes its ignition state
  708. local function store_portal_location_info(portal_name, anchorPos, orientation, ignited)
  709. if not DEBUG_IGNORE_MODSTORAGE then
  710. local key = minetest.pos_to_string(anchorPos) .. " is " .. portal_name
  711. if DEBUG then minetest.chat_send_all("Adding/updating portal in mod_storage: " .. key) end
  712. mod_storage:set_string(
  713. key,
  714. minetest.serialize({orientation = orientation, active = ignited})
  715. )
  716. end
  717. end
  718. -- Remove portal information from mod storage.
  719. -- Do this if a portal frame is destroyed such that it cannot be ignited anymore.
  720. local function remove_portal_location_info(portal_name, anchorPos)
  721. if not DEBUG_IGNORE_MODSTORAGE then
  722. local key = minetest.pos_to_string(anchorPos) .. " is " .. portal_name
  723. if DEBUG then minetest.chat_send_all("Removing portal from mod_storage: " .. key) end
  724. mod_storage:set_string(key, "")
  725. end
  726. end
  727. -- Returns a table of the nearest portals to anchorPos indexed by distance, based on mod_storage
  728. -- data.
  729. -- Only portals in the same realm as the anchorPos will be returned, even if y_factor is 0.
  730. -- WARNING: Portals are not checked, and inactive portals especially may have been damaged without
  731. -- being removed from the mod_storage data. Check these portals still exist before using them, and
  732. -- invoke remove_portal_location_info() on any found to no longer exist.
  733. --
  734. -- A y_factor of 0 means y does not affect the distance_limit, a y_factor of 1 means y is included,
  735. -- and a y_factor of 2 would squash the search-sphere by a factor of 2 on the y-axis, etc.
  736. -- Pass a nil or negative distance_limit to indicate no distance limit
  737. local function list_closest_portals(portal_definition, anchorPos, distance_limit, y_factor)
  738. local result = {}
  739. if not DEBUG_IGNORE_MODSTORAGE then
  740. local isRealm = portal_definition.is_within_realm(anchorPos)
  741. if distance_limit == nil then distance_limit = -1 end
  742. if y_factor == nil then y_factor = 1 end
  743. for key, value in pairs(mod_storage:to_table().fields) do
  744. local closingBrace = key:find(")", 6, true)
  745. if closingBrace ~= nil then
  746. local found_anchorPos = minetest.string_to_pos(key:sub(0, closingBrace))
  747. if found_anchorPos ~= nil and portal_definition.is_within_realm(found_anchorPos) == isRealm then
  748. local found_name = key:sub(closingBrace + 5)
  749. if found_name == portal_definition.name then
  750. local x = anchorPos.x - found_anchorPos.x
  751. local y = anchorPos.y - found_anchorPos.y
  752. local z = anchorPos.z - found_anchorPos.z
  753. local distance = math.hypot(y * y_factor, math.hypot(x, z))
  754. if distance <= distance_limit or distance_limit < 0 then
  755. local info = minetest.deserialize(value) or {}
  756. if DEBUG then minetest.chat_send_all("found " .. found_name .. " listed at distance " .. distance .. " (within " .. distance_limit .. ") from dest " .. minetest.pos_to_string(anchorPos) .. ", found: " .. minetest.pos_to_string(found_anchorPos) .. " orientation " .. info.orientation) end
  757. info.anchorPos = found_anchorPos
  758. info.distance = distance
  759. result[distance] = info
  760. end
  761. end
  762. end
  763. end
  764. end
  765. end
  766. return result
  767. end
  768. -- the timerNode is used to keep the metadata as that node already needs to be known any time a portal is stopped or run
  769. -- see also ambient_sound_stop()
  770. function ambient_sound_play(portal_definition, soundPos, timerNodeMeta)
  771. if portal_definition.sounds.ambient ~= nil then
  772. local soundLength = portal_definition.sounds.ambient.length
  773. if soundLength == nil then soundLength = 3 end
  774. local lastPlayed = timerNodeMeta:get_int("ambient_sound_last_played")
  775. -- Using "os.time() % soundLength == 0" is lightweight but means delayed starts, so trying a stored lastPlayed
  776. if os.time() >= lastPlayed + soundLength then
  777. local soundHandle = minetest.sound_play(portal_definition.sounds.ambient, {pos = soundPos, max_hear_distance = 8})
  778. if timerNodeMeta ~= nil then
  779. timerNodeMeta:set_int("ambient_sound_handle", soundHandle)
  780. timerNodeMeta:set_int("ambient_sound_last_played", os.time())
  781. end
  782. end
  783. end
  784. end
  785. -- the timerNode is used to keep the metadata as that node already needs to be known any time a portal is stopped or run
  786. -- see also ambient_sound_play()
  787. function ambient_sound_stop(timerNodeMeta)
  788. if timerNodeMeta ~= nil then
  789. local soundHandle = timerNodeMeta:get_int("ambient_sound_handle")
  790. minetest.sound_fade(soundHandle, -3, 0)
  791. -- clear the metadata
  792. timerNodeMeta:set_string("ambient_sound_handle", "")
  793. timerNodeMeta:set_string("ambient_sound_last_played", "")
  794. end
  795. end
  796. -- WARNING - this is invoked by on_destruct, so you can't assume there's an accesible node at pos
  797. -- Returns true if a portal was found to extinguish
  798. function extinguish_portal(pos, node_name, frame_was_destroyed)
  799. -- mesecons seems to invoke action_off() 6 times every time you place a block?
  800. if DEBUG then minetest.chat_send_all("extinguish_portal" .. minetest.pos_to_string(pos) .. " " .. node_name) end
  801. local meta = minetest.get_meta(pos)
  802. local p1 = minetest.string_to_pos(meta:get_string("p1"))
  803. local p2 = minetest.string_to_pos(meta:get_string("p2"))
  804. local target = minetest.string_to_pos(meta:get_string("target"))
  805. if p1 == nil or p2 == nil then
  806. if DEBUG then minetest.chat_send_all(" no active portal found to extinguish") end
  807. return false
  808. end
  809. local portal_definition = get_portal_definition(node_name, p1, p2)
  810. if portal_definition == nil then
  811. minetest.log("error", "extinguish_portal() invoked on " .. node_name .. " but no registered portal is constructed from " .. node_name)
  812. return false -- no portal frames are made from this type of node
  813. end
  814. if portal_definition.sounds.extinguish ~= nil then
  815. minetest.sound_play(portal_definition.sounds.extinguish, {pos = p1})
  816. end
  817. -- stop timer and ambient sound
  818. local timerPos = get_timerPos_from_p1_and_p2(p1, p2)
  819. minetest.get_node_timer(timerPos):stop()
  820. ambient_sound_stop(minetest.get_meta(timerPos))
  821. -- update the ignition state in the portal location info
  822. local anchorPos, orientation = portal_definition.shape.get_anchorPos_and_orientation_from_p1_and_p2(p1, p2)
  823. if frame_was_destroyed then
  824. remove_portal_location_info(portal_definition.name, anchorPos)
  825. else
  826. store_portal_location_info(portal_definition.name, anchorPos, orientation, false)
  827. end
  828. local frame_node_name = portal_definition.frame_node_name
  829. local wormhole_node_name = portal_definition.wormhole_node_name
  830. for x = p1.x, p2.x do
  831. for y = p1.y, p2.y do
  832. for z = p1.z, p2.z do
  833. local clearPos = {x = x, y = y, z = z}
  834. local nn = minetest.get_node(clearPos).name
  835. if nn == frame_node_name or nn == wormhole_node_name then
  836. if nn == wormhole_node_name then
  837. minetest.remove_node(clearPos)
  838. if meseconsAvailable then mesecon.receptor_off(clearPos) end
  839. end
  840. local m = minetest.get_meta(clearPos)
  841. m:set_string("p1", "")
  842. m:set_string("p2", "")
  843. m:set_string("target", "")
  844. m:set_string("portal_type", "")
  845. end
  846. end
  847. end
  848. end
  849. if target ~= nil then
  850. if DEBUG then minetest.chat_send_all(" attempting to also extinguish target with wormholePos " .. minetest.pos_to_string(target)) end
  851. extinguish_portal(target, node_name)
  852. end
  853. if portal_definition.on_extinguish ~= nil then
  854. portal_definition.on_extinguish(portal_definition, anchorPos, orientation)
  855. end
  856. return true
  857. end
  858. -- Note: will extinguish any portal using the same nodes that are being set
  859. local function set_portal_metadata(portal_definition, anchorPos, orientation, destination_wormholePos, ignite)
  860. if DEBUG then minetest.chat_send_all("set_portal_metadata(ignite=" .. tostring(ignite) .. ") at " .. minetest.pos_to_string(anchorPos) .. " orient " .. orientation .. ", setting to target " .. minetest.pos_to_string(destination_wormholePos)) end
  861. -- Portal position is stored in metadata as p1 and p2 to keep maps compatible with earlier versions of this mod.
  862. -- p1 is the bottom/west/south corner of the portal, and p2 is the opposite corner, together
  863. -- they define the bounding volume for the portal.
  864. local p1, p2 = portal_definition.shape:get_p1_and_p2_from_anchorPos(anchorPos, orientation)
  865. local p1_string, p2_string = minetest.pos_to_string(p1), minetest.pos_to_string(p2)
  866. local param2 = get_colorfacedir_from_color_and_orientation(portal_definition.wormhole_node_color, orientation, portal_definition.shape.is_horizontal)
  867. local mesecon_rules
  868. if ignite and meseconsAvailable then mesecon_rules = get_mesecon_emission_rules_from_colorfacedir(param2) end
  869. local update_aborted-- using closures to allow the updateFunc to return extra information - by setting this variable
  870. local updateFunc = function(pos)
  871. local meta = minetest.get_meta(pos)
  872. if ignite then
  873. local node_name = minetest.get_node(pos).name
  874. if node_name == "air" then
  875. minetest.set_node(pos, {name = portal_definition.wormhole_node_name, param2 = param2})
  876. if meseconsAvailable then mesecon.receptor_on(pos, mesecon_rules) end
  877. end
  878. local existing_p1 = meta:get_string("p1")
  879. if existing_p1 ~= "" then
  880. local existing_p2 = meta:get_string("p2")
  881. if existing_p1 ~= p1_string or existing_p2 ~= p2_string then
  882. if DEBUG then minetest.chat_send_all("set_portal_metadata() found existing metadata from another portal: existing_p1 " .. existing_p1 .. ", existing_p2" .. existing_p2 .. ", p1 " .. p1_string .. ", p2 " .. p2_string .. ", will existinguish existing portal...") end
  883. -- this node is already part of another portal, so extinguish that, because nodes only
  884. -- contain a link in the metadata to one portal, and being part of two allows a slew of bugs
  885. extinguish_portal(pos, node_name, false)
  886. -- clear the metadata to avoid causing a loop if extinguish_portal() fails on this node (e.g. it only works on frame nodes)
  887. meta:set_string("p1", nil)
  888. meta:set_string("p2", nil)
  889. meta:set_string("target", nil)
  890. meta:set_string("portal_type", nil)
  891. update_aborted = true
  892. return true -- short-circuit the update
  893. end
  894. end
  895. end
  896. meta:set_string("p1", minetest.pos_to_string(p1))
  897. meta:set_string("p2", minetest.pos_to_string(p2))
  898. meta:set_string("target", minetest.pos_to_string(destination_wormholePos))
  899. if portal_definition.name ~= "nether_portal" then
  900. -- Legacy portals won't have this extra metadata, so don't rely on it.
  901. -- It's not strictly necessary for PortalShape_Traditional as we know that p1 is part of
  902. -- the frame and we can look up the portal type from p1, p2, and frame node name.
  903. -- Being able to read this from the metadata means other portal shapes needn't have their
  904. -- frame at the timerPos, it may handle unloaded nodes better, and it saves an extra call
  905. -- to minetest.getnode().
  906. meta:set_string("portal_type", portal_definition.name)
  907. end
  908. end
  909. repeat
  910. update_aborted = false
  911. portal_definition.shape.apply_func_to_frame_nodes(anchorPos, orientation, updateFunc)
  912. portal_definition.shape.apply_func_to_wormhole_nodes(anchorPos, orientation, updateFunc)
  913. until not update_aborted
  914. local timerPos = get_timerPos_from_p1_and_p2(p1, p2)
  915. minetest.get_node_timer(timerPos):start(1)
  916. store_portal_location_info(portal_definition.name, anchorPos, orientation, true)
  917. end
  918. local function set_portal_metadata_and_ignite(portal_definition, anchorPos, orientation, destination_wormholePos)
  919. set_portal_metadata(portal_definition, anchorPos, orientation, destination_wormholePos, true)
  920. end
  921. -- this function returns two bools: portal found, portal is lit
  922. local function is_portal_at_anchorPos(portal_definition, anchorPos, orientation, force_chunk_load)
  923. local nodes_are_valid -- using closures to allow the check functions to return extra information - by setting this variable
  924. local portal_is_ignited -- using closures to allow the check functions to return extra information - by setting this variable
  925. local frame_node_name = portal_definition.frame_node_name
  926. local check_frame_Func = function(check_pos)
  927. local foundName = minetest.get_node(check_pos).name
  928. if foundName ~= frame_node_name then
  929. if force_chunk_load and foundName == "ignore" then
  930. -- area isn't loaded, force loading/emerge of check area
  931. minetest.get_voxel_manip():read_from_map(check_pos, check_pos)
  932. foundName = minetest.get_node(check_pos).name
  933. if DEBUG then minetest.chat_send_all("Forced loading of 'ignore' node at " .. minetest.pos_to_string(check_pos) .. ", got " .. foundName) end
  934. if foundName ~= frame_node_name then
  935. nodes_are_valid = false
  936. return true -- short-circuit the search
  937. end
  938. else
  939. nodes_are_valid = false
  940. return true -- short-circuit the search
  941. end
  942. end
  943. end
  944. local wormhole_node_name = portal_definition.wormhole_node_name
  945. local check_wormhole_Func = function(check_pos)
  946. local node_name = minetest.get_node(check_pos).name
  947. if node_name ~= wormhole_node_name then
  948. portal_is_ignited = false;
  949. if node_name ~= "air" then
  950. nodes_are_valid = false
  951. return true -- short-circuit the search
  952. end
  953. end
  954. end
  955. nodes_are_valid = true
  956. portal_is_ignited = true
  957. portal_definition.shape.apply_func_to_frame_nodes(anchorPos, orientation, check_frame_Func) -- check_frame_Func affects nodes_are_valid, portal_is_ignited
  958. if nodes_are_valid then
  959. -- a valid frame exists at anchorPos, check the wormhole is either ignited or unobstructed
  960. portal_definition.shape.apply_func_to_wormhole_nodes(anchorPos, orientation, check_wormhole_Func) -- check_wormhole_Func affects nodes_are_valid, portal_is_ignited
  961. end
  962. return nodes_are_valid, portal_is_ignited and nodes_are_valid -- returns two bools: portal was found, portal is lit
  963. end
  964. -- Checks pos, and if it's part of a portal or portal frame then three values are returned: anchorPos, orientation, is_ignited
  965. -- where orientation is 0 or 90 (0 meaning a portal that faces north/south - i.e. obsidian running east/west)
  966. local function is_within_portal_frame(portal_definition, pos)
  967. local width_minus_1 = portal_definition.shape.size.x - 1
  968. local height_minus_1 = portal_definition.shape.size.y - 1
  969. local depth_minus_1 = portal_definition.shape.size.z - 1
  970. for d = -depth_minus_1, depth_minus_1 do
  971. for w = -width_minus_1, width_minus_1 do
  972. for y = -height_minus_1, height_minus_1 do
  973. local testAnchorPos_x = {x = pos.x + w, y = pos.y + y, z = pos.z + d}
  974. local portal_found, portal_lit = is_portal_at_anchorPos(portal_definition, testAnchorPos_x, 0, true)
  975. if portal_found then
  976. return testAnchorPos_x, 0, portal_lit
  977. else
  978. -- try orthogonal orientation
  979. local testForAnchorPos_z = {x = pos.x + d, y = pos.y + y, z = pos.z + w}
  980. portal_found, portal_lit = is_portal_at_anchorPos(portal_definition, testForAnchorPos_z, 90, true)
  981. if portal_found then return testForAnchorPos_z, 90, portal_lit end
  982. end
  983. end
  984. end
  985. end
  986. end
  987. -- sets param2 values in the schematic to match facedir values, or 0 if the portalframe-nodedef doesn't use facedir
  988. local function set_schematic_param2(schematic_table, frame_node_name, frame_node_color)
  989. local paramtype2 = minetest.registered_nodes[frame_node_name].paramtype2
  990. local isFacedir = paramtype2 == "facedir" or paramtype2 == "colorfacedir"
  991. if schematic_table.facedirNodes ~= nil then
  992. for _, node in ipairs(schematic_table.facedirNodes) do
  993. if isFacedir and node.facedir ~= nil then
  994. -- frame_node_color can be nil
  995. local colorBits = (frame_node_color or math.floor((node.param2 or 0) / 32)) * 32
  996. node.param2 = node.facedir + colorBits
  997. else
  998. node.param2 = 0
  999. end
  1000. end
  1001. end
  1002. end
  1003. local function build_portal(portal_definition, anchorPos, orientation, destination_wormholePos)
  1004. set_schematic_param2(portal_definition.shape.schematic, portal_definition.frame_node_name, portal_definition.frame_node_color)
  1005. minetest.place_schematic(
  1006. portal_definition.shape.get_schematicPos_from_anchorPos(anchorPos, orientation),
  1007. portal_definition.shape.schematic,
  1008. orientation,
  1009. { -- node replacements
  1010. ["default:obsidian"] = portal_definition.frame_node_name,
  1011. },
  1012. true
  1013. )
  1014. -- set the param2 on wormhole nodes to ensure they are the right color
  1015. local wormholeNode = {
  1016. name = portal_definition.wormhole_node_name,
  1017. param2 = get_colorfacedir_from_color_and_orientation(portal_definition.wormhole_node_color, orientation, portal_definition.shape.is_horizontal)
  1018. }
  1019. portal_definition.shape.apply_func_to_wormhole_nodes(
  1020. anchorPos,
  1021. orientation,
  1022. function(pos) minetest.swap_node(pos, wormholeNode) end
  1023. )
  1024. if DEBUG then minetest.chat_send_all("Placed " .. portal_definition.name .. " portal schematic at " .. minetest.pos_to_string(portal_definition.shape.get_schematicPos_from_anchorPos(anchorPos, orientation)) .. ", orientation " .. orientation) end
  1025. set_portal_metadata(portal_definition, anchorPos, orientation, destination_wormholePos)
  1026. if portal_definition.on_created ~= nil then
  1027. portal_definition.on_created(portal_definition, anchorPos, orientation)
  1028. end
  1029. end
  1030. -- Sometimes after a portal is placed, concurrent mapgen routines overwrite it.
  1031. -- Make portals immortal for ~20 seconds after creation
  1032. local function remote_portal_checkup(elapsed, portal_definition, anchorPos, orientation, destination_wormholePos)
  1033. if DEBUG then minetest.chat_send_all("portal checkup at " .. elapsed .. " seconds") end
  1034. local wormholePos = portal_definition.shape.get_wormholePos_from_anchorPos(anchorPos, orientation)
  1035. local wormhole_node = minetest.get_node_or_nil(wormholePos)
  1036. local portalFound, portalLit = false, false
  1037. if wormhole_node ~= nil and wormhole_node.name == portal_definition.wormhole_node_name then
  1038. -- a wormhole node was there, but check the whole frame is intact
  1039. portalFound, portalLit = is_portal_at_anchorPos(portal_definition, anchorPos, orientation, false)
  1040. end
  1041. if not portalFound or not portalLit then
  1042. -- ruh roh
  1043. local message = "Newly created portal at " .. minetest.pos_to_string(anchorPos) .. " was overwritten. Attempting to recreate. Issue spotted after " .. elapsed .. " seconds"
  1044. minetest.log("warning", message)
  1045. if DEBUG then minetest.chat_send_all("!!! " .. message) end
  1046. -- A pre-existing portal frame wouldn't have been immediately overwritten, so no need to check for one, just place the portal.
  1047. build_portal(portal_definition, anchorPos, orientation, destination_wormholePos)
  1048. end
  1049. if elapsed < 10 then -- stop checking after ~20 seconds
  1050. local delay = elapsed * 2
  1051. minetest.after(delay, remote_portal_checkup, elapsed + delay, portal_definition, anchorPos, orientation, destination_wormholePos)
  1052. end
  1053. end
  1054. -- Used to find or build the remote twin after a portal is opened.
  1055. -- If a portal is found that is already lit then it will be extinguished first and its destination_wormholePos updated,
  1056. -- this is to enforce that portals only link together in mutual pairs. It would be better for gameplay if I didn't apply
  1057. -- that restriction, but it would require maintaining an accurate list of every portal that links to a portal so they
  1058. -- could be updated if the portal is destroyed. To keep the code simple I'm going to limit portals to only being the
  1059. -- destination of one lit portal at a time.
  1060. -- * suggested_wormholePos indicates where the portal should be built - note this not an anchorPos!
  1061. -- * suggested_orientation is the suggested schematic rotation to use if no useable portal is found at suggested_wormholePos:
  1062. -- 0, 90, 180, 270 (0 meaning a portal that faces north/south - i.e. obsidian running east/west)
  1063. -- * destination_wormholePos is the wormholePos of the destination portal this one will be linked to.
  1064. --
  1065. -- Returns the final (anchorPos, orientation), as they may differ from the anchorPos and orientation that was
  1066. -- specified if an existing portal was already found there.
  1067. local function locate_or_build_portal(portal_definition, suggested_wormholePos, suggested_orientation, destination_wormholePos)
  1068. if DEBUG then minetest.chat_send_all("locate_or_build_portal() called at wormholePos" .. minetest.pos_to_string(suggested_wormholePos) .. " with suggested orient " .. suggested_orientation .. ", targetted to " .. minetest.pos_to_string(destination_wormholePos)) end
  1069. local result_anchorPos;
  1070. local result_orientation;
  1071. -- Searching for an existing portal at wormholePos seems better than at anchorPos, though isn't important
  1072. local found_anchorPos, found_orientation, is_ignited = is_within_portal_frame(portal_definition, suggested_wormholePos) -- can be optimized - check for portal at exactly suggested_wormholePos first
  1073. if found_anchorPos ~= nil then
  1074. -- A portal is already here, we don't have to build one, though we may need to ignite it
  1075. result_anchorPos = found_anchorPos
  1076. result_orientation = found_orientation
  1077. if is_ignited then
  1078. -- We're about to link to this portal, so if it's already linked to a different portal then
  1079. -- extinguish it, to update the state of the about-to-be-orphaned portal.
  1080. local result_target_str = minetest.get_meta(result_anchorPos):get_string("target")
  1081. local result_target = minetest.string_to_pos(result_target_str)
  1082. if result_target ~= nil and vector.equals(result_target, destination_wormholePos) then
  1083. -- It already links back to the portal the player is teleporting from, so don't
  1084. -- extinguish it or the player's portal will also extinguish.
  1085. if DEBUG then minetest.chat_send_all(" Build unnecessary: already a lit portal that links back here at " .. minetest.pos_to_string(found_anchorPos) .. ", orientation " .. result_orientation) end
  1086. else
  1087. if DEBUG then minetest.chat_send_all(" Build unnecessary: already a lit portal at " .. minetest.pos_to_string(found_anchorPos) .. ", orientation " .. result_orientation .. ", linking to " .. result_target_str .. ". Extinguishing...") end
  1088. extinguish_portal(found_anchorPos, portal_definition.frame_node_name, false)
  1089. end
  1090. else
  1091. if DEBUG then minetest.chat_send_all(" Build unnecessary: already an unlit portal at " .. minetest.pos_to_string(found_anchorPos) .. ", orientation " .. result_orientation) end
  1092. end
  1093. -- ignite the portal
  1094. set_portal_metadata_and_ignite(portal_definition, result_anchorPos, result_orientation, destination_wormholePos)
  1095. else
  1096. result_orientation = suggested_orientation
  1097. result_anchorPos = portal_definition.shape.get_anchorPos_from_wormholePos(suggested_wormholePos, result_orientation)
  1098. build_portal(portal_definition, result_anchorPos, result_orientation, destination_wormholePos)
  1099. -- make sure portal isn't overwritten by ongoing generation/emerge
  1100. minetest.after(2, remote_portal_checkup, 2, portal_definition, result_anchorPos, result_orientation, destination_wormholePos)
  1101. end
  1102. return result_anchorPos, result_orientation
  1103. end
  1104. -- invoked when a player attempts to turn obsidian nodes into an open portal
  1105. -- ignition_node_name is optional
  1106. local function ignite_portal(ignition_pos, ignition_node_name)
  1107. if ignition_node_name == nil then ignition_node_name = minetest.get_node(ignition_pos).name end
  1108. if DEBUG then minetest.chat_send_all("IGNITE the " .. ignition_node_name .. " at " .. minetest.pos_to_string(ignition_pos)) end
  1109. -- find which sort of portals are made from the node that was clicked on
  1110. local portal_definition_list = list_portal_definitions_for_frame_node(ignition_node_name)
  1111. for _, portal_definition in ipairs(portal_definition_list) do
  1112. local continue = false -- WRT the for loop, since lua has no continue keyword
  1113. -- check it was a portal frame that the player is trying to ignite
  1114. local anchorPos, orientation, is_ignited = is_within_portal_frame(portal_definition, ignition_pos)
  1115. if anchorPos == nil then
  1116. if DEBUG then minetest.chat_send_all("No " .. portal_definition.name .. " portal frame found at " .. minetest.pos_to_string(ignition_pos)) end
  1117. continue = true -- no portal is here, but perhaps there's more than one portal type we need to search for
  1118. elseif is_ignited then
  1119. -- Found a portal, check its metadata and timer is healthy.
  1120. local repair = false
  1121. local meta = minetest.get_meta(ignition_pos)
  1122. if meta ~= nil then
  1123. local p1, p2, target = meta:get_string("p1"), meta:get_string("p2"), meta:get_string("target")
  1124. if p1 == "" or p2 == "" or target == "" then
  1125. -- metadata is missing, the portal frame node must have been removed without calling
  1126. -- on_destruct - perhaps by an ABM, then replaced - presumably by a player.
  1127. -- allowing reigniting will repair the portal
  1128. if DEBUG then minetest.chat_send_all("Broken portal detected, allowing reignition/repair") end
  1129. repair = true
  1130. else
  1131. if DEBUG then minetest.chat_send_all("This portal links to " .. meta:get_string("target") .. ". p1=" .. meta:get_string("p1") .. " p2=" .. meta:get_string("p2")) end
  1132. -- Check the portal's timer is running, and fix if it's not.
  1133. -- A portal's timer can stop running if the game is played without that portal type being
  1134. -- registered, e.g. enabling one of the example portals then later disabling it, then enabling it again.
  1135. -- (if this is a frequent problem, then change the value of "run_at_every_load" in the lbm)
  1136. local timer = minetest.get_node_timer(get_timerPos_from_p1_and_p2(minetest.string_to_pos(p1), minetest.string_to_pos(p2)))
  1137. if timer ~= nil and timer:get_timeout() == 0 then
  1138. if DEBUG then minetest.chat_send_all("Portal timer was not running: restarting the timer.") end
  1139. timer:start(1)
  1140. end
  1141. end
  1142. end
  1143. if not repair then return false end -- portal is already ignited (or timer has been fixed)
  1144. end
  1145. if continue == false then
  1146. if DEBUG then minetest.chat_send_all("Found portal frame. Looked at " .. minetest.pos_to_string(ignition_pos) .. ", found at " .. minetest.pos_to_string(anchorPos) .. " orientation " .. orientation) end
  1147. local destination_anchorPos, destination_orientation
  1148. if portal_definition.is_within_realm(ignition_pos) then
  1149. destination_anchorPos, destination_orientation = portal_definition.find_surface_anchorPos(anchorPos)
  1150. else
  1151. destination_anchorPos, destination_orientation = portal_definition.find_realm_anchorPos(anchorPos)
  1152. end
  1153. if DEBUG and destination_orientation == nil then minetest.chat_send_all("No destination_orientation given") end
  1154. if destination_orientation == nil then destination_orientation = orientation end
  1155. if destination_anchorPos == nil then
  1156. if DEBUG then minetest.chat_send_all("No portal destination available here!") end
  1157. return false
  1158. else
  1159. local destination_wormholePos = portal_definition.shape.get_wormholePos_from_anchorPos(destination_anchorPos, destination_orientation)
  1160. if DEBUG then minetest.chat_send_all("Destination set to " .. minetest.pos_to_string(destination_anchorPos)) end
  1161. -- ignition/BURN_BABY_BURN
  1162. set_portal_metadata_and_ignite(portal_definition, anchorPos, orientation, destination_wormholePos)
  1163. if portal_definition.sounds.ignite ~= nil then
  1164. local local_wormholePos = portal_definition.shape.get_wormholePos_from_anchorPos(anchorPos, orientation)
  1165. minetest.sound_play(portal_definition.sounds.ignite, {pos = local_wormholePos, max_hear_distance = 20})
  1166. end
  1167. if portal_definition.on_ignite ~= nil then
  1168. portal_definition.on_ignite(portal_definition, anchorPos, orientation)
  1169. end
  1170. return true
  1171. end
  1172. end
  1173. end
  1174. end
  1175. -- invoked when a player is standing in a portal
  1176. local function ensure_remote_portal_then_teleport(playerName, portal_definition, local_anchorPos, local_orientation, destination_wormholePos)
  1177. local player = minetest.get_player_by_name(playerName)
  1178. if player == nil then return end -- player quit the game while teleporting
  1179. local playerPos = player:get_pos()
  1180. if playerPos == nil then return end -- player quit the game while teleporting
  1181. -- check player is still standing in a portal
  1182. playerPos.y = playerPos.y + 0.1 -- Fix some glitches at -8000
  1183. if minetest.get_node(playerPos).name ~= portal_definition.wormhole_node_name then
  1184. return -- the player has moved out of the portal
  1185. end
  1186. -- debounce - check player is still standing in the *same* portal that called this function
  1187. local meta = minetest.get_meta(playerPos)
  1188. local local_p1, local_p2 = portal_definition.shape:get_p1_and_p2_from_anchorPos(local_anchorPos, local_orientation)
  1189. local p1_at_playerPos = minetest.string_to_pos(meta:get_string("p1"))
  1190. if p1_at_playerPos == nil or not vector.equals(local_p1, p1_at_playerPos) then
  1191. if DEBUG then minetest.chat_send_all("the player already teleported from " .. minetest.pos_to_string(local_anchorPos) .. ", and is now standing in a different portal - " .. meta:get_string("p1")) end
  1192. return -- the player already teleported, and is now standing in a different portal
  1193. end
  1194. local dest_wormhole_node = minetest.get_node_or_nil(destination_wormholePos)
  1195. if dest_wormhole_node == nil then
  1196. -- area not emerged yet, delay and retry
  1197. if DEBUG then minetest.chat_send_all("ensure_remote_portal_then_teleport() could not find anything yet at " .. minetest.pos_to_string(destination_wormholePos)) end
  1198. minetest.after(1, ensure_remote_portal_then_teleport, playerName, portal_definition, local_anchorPos, local_orientation, destination_wormholePos)
  1199. else
  1200. local local_wormholePos = portal_definition.shape.get_wormholePos_from_anchorPos(local_anchorPos, local_orientation)
  1201. if dest_wormhole_node.name == portal_definition.wormhole_node_name then
  1202. -- portal exists
  1203. local destination_orientation = get_orientation_from_colorfacedir(dest_wormhole_node.param2)
  1204. local destination_anchorPos = portal_definition.shape.get_anchorPos_from_wormholePos(destination_wormholePos, destination_orientation)
  1205. portal_definition.shape.disable_portal_trap(destination_anchorPos, destination_orientation)
  1206. -- if the portal is already linked to a different portal then extinguish the other portal and
  1207. -- update the target portal to point back at this one.
  1208. local remoteMeta = minetest.get_meta(destination_wormholePos)
  1209. local remoteTarget = minetest.string_to_pos(remoteMeta:get_string("target"))
  1210. if remoteTarget == nil then
  1211. if DEBUG then minetest.chat_send_all("Failed to test whether target portal links back to this one") end
  1212. elseif not vector.equals(remoteTarget, local_wormholePos) then
  1213. if DEBUG then minetest.chat_send_all("Target portal is already linked, extinguishing then relighting to point back at this one") end
  1214. extinguish_portal(remoteTarget, portal_definition.frame_node_name, false)
  1215. set_portal_metadata_and_ignite(
  1216. portal_definition,
  1217. destination_anchorPos,
  1218. destination_orientation,
  1219. local_wormholePos
  1220. )
  1221. end
  1222. if DEBUG then minetest.chat_send_all("Teleporting player from wormholePos" .. minetest.pos_to_string(local_wormholePos) .. " to wormholePos" .. minetest.pos_to_string(destination_wormholePos)) end
  1223. -- play the teleport sound
  1224. if portal_definition.sounds.teleport ~= nil then
  1225. minetest.sound_play(portal_definition.sounds.teleport, {to_player = playerName})
  1226. end
  1227. -- rotate the player if the destination portal is a different orientation
  1228. local rotation_angle = math.rad(destination_orientation - local_orientation)
  1229. local offset = vector.subtract(playerPos, local_wormholePos) -- preserve player's position in the portal
  1230. local rotated_offset = {x = math.cos(rotation_angle) * offset.x - math.sin(rotation_angle) * offset.z, y = offset.y, z = math.sin(rotation_angle) * offset.x + math.cos(rotation_angle) * offset.z}
  1231. local new_playerPos = vector.add(destination_wormholePos, rotated_offset)
  1232. player:set_pos(new_playerPos)
  1233. player:set_look_horizontal(player:get_look_horizontal() + rotation_angle)
  1234. if portal_definition.on_player_teleported ~= nil then
  1235. portal_definition.on_player_teleported(portal_definition, player, playerPos, new_playerPos)
  1236. end
  1237. else
  1238. -- no wormhole node at destination - destination portal either needs to be built or ignited.
  1239. -- Note: A very rare edge-case that is difficult to set up:
  1240. -- If the destination portal is unlit and its frame shares a node with a lit portal that is linked to this
  1241. -- portal (but has not been travelled through, thus not linking this portal back to it), then igniting
  1242. -- the destination portal will extinguish the portal it's touching, which will extinguish this portal
  1243. -- which will leave a confused player.
  1244. -- I don't think this is worth preventing, but I document it incase someone describes entering a portal
  1245. -- and then the portal turning off.
  1246. if DEBUG then minetest.chat_send_all("ensure_remote_portal_then_teleport() saw " .. dest_wormhole_node.name .. " at " .. minetest.pos_to_string(destination_wormholePos) .. " rather than a wormhole. Calling locate_or_build_portal()") end
  1247. local new_dest_anchorPos, new_dest_orientation = locate_or_build_portal(portal_definition, destination_wormholePos, local_orientation, local_wormholePos)
  1248. local new_dest_wormholePos = portal_definition.shape.get_wormholePos_from_anchorPos(new_dest_anchorPos, new_dest_orientation)
  1249. if not vector.equals(destination_wormholePos, new_dest_wormholePos) then
  1250. -- Update the local portal's target to match where the existing remote portal was found
  1251. if minetest.get_meta(local_anchorPos):get_string("target") == "" then
  1252. -- The local portal has been extinguished!
  1253. -- Abort setting its metadata as that assumes it is active.
  1254. -- This shouldn't happen and may indicate a bug, I trap it incase when the destination
  1255. -- portal was found and extinguished, it somehow linked back to the local portal in a
  1256. -- misaligned fashion that wasn't recognized as being the local portal and caused the
  1257. -- local portal to also be extinguished.
  1258. local message = "Local portal at " .. minetest.pos_to_string(local_anchorPos) .. " was extinguished while linking to existing portal at " .. minetest.pos_to_string(new_dest_anchorPos)
  1259. minetest.log("error", message)
  1260. if DEBUG then minetest.chat_send_all("!ERROR! - " .. message) end
  1261. else
  1262. destination_wormholePos = new_dest_wormholePos
  1263. if DEBUG then minetest.chat_send_all(" updating target to where remote portal was found - " .. minetest.pos_to_string(destination_wormholePos)) end
  1264. set_portal_metadata(
  1265. portal_definition,
  1266. local_anchorPos,
  1267. local_orientation,
  1268. destination_wormholePos
  1269. )
  1270. end
  1271. end
  1272. minetest.after(0.1, ensure_remote_portal_then_teleport, playerName, portal_definition, local_anchorPos, local_orientation, destination_wormholePos)
  1273. end
  1274. end
  1275. end
  1276. -- run_wormhole() is invoked once per second per portal, handling teleportation and particle effects.
  1277. -- See get_timerPos_from_p1_and_p2() for an explanation of the timerPos location
  1278. function run_wormhole(timerPos, time_elapsed)
  1279. local portal_definition -- will be used inside run_wormhole_node_func()
  1280. local run_wormhole_node_func = function(pos)
  1281. if math.random(2) == 1 then -- lets run only 3 particlespawners instead of 6 per portal
  1282. minetest.add_particlespawner({
  1283. amount = 16,
  1284. time = 2,
  1285. minpos = {x = pos.x - 0.25, y = pos.y - 0.25, z = pos.z - 0.25},
  1286. maxpos = {x = pos.x + 0.25, y = pos.y + 0.25, z = pos.z + 0.25},
  1287. minvel = {x = -0.8, y = -0.8, z = -0.8},
  1288. maxvel = {x = 0.8, y = 0.8, z = 0.8},
  1289. minacc = {x = 0, y = 0, z = 0},
  1290. maxacc = {x = 0, y = 0, z = 0},
  1291. minexptime = 0.5,
  1292. maxexptime = 1.7,
  1293. minsize = 0.5 * portal_definition.particle_texture_scale,
  1294. maxsize = 1.5 * portal_definition.particle_texture_scale,
  1295. collisiondetection = false,
  1296. texture = portal_definition.particle_texture_colored,
  1297. animation = portal_definition.particle_texture_animation,
  1298. glow = 5
  1299. })
  1300. end
  1301. for _, obj in ipairs(minetest.get_objects_inside_radius(pos, 1)) do
  1302. if obj:is_player() then
  1303. local meta = minetest.get_meta(pos)
  1304. local destination_wormholePos = minetest.string_to_pos(meta:get_string("target"))
  1305. local local_p1 = minetest.string_to_pos(meta:get_string("p1"))
  1306. local local_p2 = minetest.string_to_pos(meta:get_string("p2"))
  1307. if destination_wormholePos ~= nil and local_p1 ~= nil and local_p2 ~= nil then
  1308. -- force emerge of target area
  1309. minetest.get_voxel_manip():read_from_map(destination_wormholePos, destination_wormholePos) -- force load
  1310. if minetest.get_node_or_nil(destination_wormholePos) == nil then
  1311. minetest.emerge_area(vector.subtract(destination_wormholePos, 4), vector.add(destination_wormholePos, 4))
  1312. end
  1313. local local_anchorPos, local_orientation = portal_definition.shape.get_anchorPos_and_orientation_from_p1_and_p2(local_p1, local_p2)
  1314. local playerName = obj:get_player_name()
  1315. minetest.after(
  1316. 3, -- hopefully target area is emerged in 3 seconds
  1317. function()
  1318. ensure_remote_portal_then_teleport(
  1319. playerName,
  1320. portal_definition,
  1321. local_anchorPos,
  1322. local_orientation,
  1323. destination_wormholePos
  1324. )
  1325. end
  1326. )
  1327. end
  1328. end
  1329. end
  1330. end
  1331. local p1, p2, portal_name
  1332. local meta = minetest.get_meta(timerPos)
  1333. if meta ~= nil then
  1334. p1 = minetest.string_to_pos(meta:get_string("p1"))
  1335. p2 = minetest.string_to_pos(meta:get_string("p2"))
  1336. portal_name = minetest.string_to_pos(meta:get_string("portal_type")) -- don't rely on this yet until you're sure everything works with old portals that don't have this set
  1337. end
  1338. if p1 ~= nil and p2 ~= nil then
  1339. -- figure out the portal shape so we know where the wormhole nodes will be located
  1340. local frame_node_name
  1341. if portal_name ~= nil and nether.registered_portals[portal_name] ~= nil then
  1342. portal_definition = nether.registered_portals[portal_name]
  1343. else
  1344. frame_node_name = minetest.get_node(timerPos).name -- timerPos should be a frame node if the shape is traditionalPortalShape
  1345. portal_definition = get_portal_definition(frame_node_name, p1, p2)
  1346. end
  1347. if portal_definition == nil then
  1348. minetest.log("error", "No portal with a \"" .. frame_node_name .. "\" frame is registered. run_wormhole" .. minetest.pos_to_string(timerPos) .. " was invoked but that location contains \"" .. frame_node_name .. "\"")
  1349. else
  1350. local anchorPos, orientation = portal_definition.shape.get_anchorPos_and_orientation_from_p1_and_p2(p1, p2)
  1351. portal_definition.shape.apply_func_to_wormhole_nodes(anchorPos, orientation, run_wormhole_node_func)
  1352. if portal_definition.on_run_wormhole ~= nil then
  1353. portal_definition.on_run_wormhole(portal_definition, anchorPos, orientation)
  1354. end
  1355. local wormholePos = portal_definition.shape.get_wormholePos_from_anchorPos(anchorPos, orientation)
  1356. ambient_sound_play(portal_definition, wormholePos, meta)
  1357. end
  1358. end
  1359. end
  1360. local function create_book(item_name, inventory_description, inventory_image, title, author, chapters)
  1361. local display_book = function(itemstack, user, pointed_thing)
  1362. local player_name = user:get_player_name()
  1363. minetest.sound_play("nether_book_open", {to_player = player_name, gain = 0.25})
  1364. local formspec =
  1365. "size[18,12.122]" ..
  1366. "background[0,0;18,11;nether_book_background.png;true]"..
  1367. "image_button_exit[17.3,0;0.8,0.8;nether_book_close.png;;]"..
  1368. "label[3.1,0.5;" .. minetest.formspec_escape(title) .. "]" ..
  1369. "label[3.6,0.9;" .. author .. "]"
  1370. local image_x_adj = -0.4
  1371. local image_width = 1.6
  1372. local image_padding = 0.06
  1373. for i, chapter in ipairs(chapters) do
  1374. local left_margin = 0.9
  1375. local top_margin = 1.7
  1376. local width = 7.9
  1377. local height = 12.0
  1378. local item_number = i
  1379. local items_on_page = math.floor(#chapters / 2)
  1380. if i > items_on_page then
  1381. -- page 2
  1382. left_margin = 10.1
  1383. top_margin = 0.8
  1384. height = 12.9
  1385. item_number = i - items_on_page
  1386. items_on_page = #chapters - items_on_page
  1387. end
  1388. local available_height = (height - top_margin) / items_on_page
  1389. local y = top_margin + (item_number - 1) * available_height
  1390. -- add chapter title
  1391. local title_height = 0
  1392. if chapter.title ~= nil then
  1393. title_height = 0.6
  1394. formspec = formspec .. "label[".. left_margin + 1.5 .. ","
  1395. .. y .. "; ──══♦♦♦◊ " .. minetest.formspec_escape(chapter.title) .. " ◊♦♦♦══──]"
  1396. end
  1397. -- add chapter image
  1398. local x_offset = 0
  1399. if chapter.image ~= nil then
  1400. x_offset = image_width + image_x_adj + image_padding
  1401. local image_height = image_width / chapter.image.width * chapter.image.height
  1402. formspec = formspec .. "image[" .. left_margin + image_x_adj .. "," .. y + title_height .. ";" .. image_width .. ","
  1403. .. image_height .. ";" .. chapter.image.image .. "]"
  1404. end
  1405. -- add chapter text
  1406. formspec = formspec .. "textarea[" .. left_margin + x_offset .. "," .. y + title_height .. ";" .. width - x_offset .. ","
  1407. .. available_height - title_height .. ";;" .. minetest.formspec_escape(chapter.text) .. ";]"
  1408. end
  1409. minetest.show_formspec(player_name, item_name, formspec)
  1410. end
  1411. minetest.register_craftitem(item_name, {
  1412. description = inventory_description,
  1413. inventory_image = inventory_image,
  1414. groups = {book = 1, not_in_creative_inventory=1},
  1415. on_use = display_book,
  1416. _doc_items_hidden = true,
  1417. _doc_items_longdesc =
  1418. S("A guidebook for how to build portals to other realms. It can sometimes be found in dungeon chests, however a copy of this book is not needed as its contents are included in this Encyclopedia.") .. "\n" ..
  1419. S("Refer: \"Help\" > \"Basics\" > \"Building Portals\""),
  1420. })
  1421. end
  1422. local function add_book_as_treasure()
  1423. book_added_as_treasure = true
  1424. if minetest.get_modpath("loot") then
  1425. loot.register_loot({
  1426. weights = { generic = nether.PORTAL_BOOK_LOOT_WEIGHTING * 1000,
  1427. books = 100 },
  1428. payload = { stack = "nether:book_of_portals" }
  1429. })
  1430. end
  1431. if minetest.get_modpath("dungeon_loot") then
  1432. dungeon_loot.register({name = "nether:book_of_portals", chance = nether.PORTAL_BOOK_LOOT_WEIGHTING})
  1433. end
  1434. -- todo: add to Treasurer mod TRMP https://github.com/poikilos/trmp_minetest_game
  1435. end
  1436. -- Returns true if the Help-modpack was installed and Portal instructions were added to it
  1437. -- Help-modpack details can be found at https://forum.minetest.net/viewtopic.php?t=15912
  1438. local function add_book_to_help_modpack(chapters)
  1439. local result = false
  1440. if minetest.get_modpath("doc") ~= nil and minetest.global_exists("doc") then
  1441. if minetest.get_modpath("doc_basics") ~= nil then
  1442. local text = S("Portals to other realms can be opened by building a frame in the right shape with the right blocks, then using an item to activate it. A local copy of the guidebook to portals is published below.\n---\n\n")
  1443. local images = {}
  1444. for i, chapter in ipairs(chapters) do
  1445. if chapter.image ~= nil then
  1446. -- Portal chapters have images (from their portalDef.shape)
  1447. text = text .. "\n\n\n" .. (i - 1) .. ") " .. chapter.title .. "\n\n"
  1448. local aspect_3_to_2_width = chapter.image.width
  1449. local aspect_3_to_2_height = aspect_3_to_2_width / 3 * 2
  1450. if chapter.image.height > aspect_3_to_2_height then
  1451. aspect_3_to_2_height = chapter.image.height
  1452. aspect_3_to_2_width = aspect_3_to_2_height / 2 * 3
  1453. end
  1454. local image_conveted_to_3_2_ratio =
  1455. "[combine:"..aspect_3_to_2_width.."x"..aspect_3_to_2_height..":0,0="..chapter.image.image
  1456. images[#images + 1] = {image=image_conveted_to_3_2_ratio}
  1457. end
  1458. text = text .. chapter.text
  1459. end
  1460. result = doc.add_entry("basics", "portals_api", {
  1461. name = S("Building Portals"),
  1462. data = {
  1463. text = text,
  1464. images = images,
  1465. aspect_ratio=.5
  1466. }
  1467. })
  1468. end
  1469. end
  1470. return result
  1471. end
  1472. -- Updates nether:book_of_portals
  1473. -- A book the player can read to lean how to build the different portals
  1474. local function create_book_of_portals()
  1475. local chapters = {}
  1476. local intro_text
  1477. -- tell the player how many portal types there are
  1478. if nether.registered_portals_count == 1 then
  1479. intro_text = S("In all my travels, and time spent in the Great Libraries, I have encountered no shortage of legends surrounding preternatural doorways said to open into other worlds, yet only one can I confirm as being more than merely a story.")
  1480. else
  1481. intro_text = S("In all my travels, and time spent in the Great Libraries, I have encountered no shortage of legends surrounding preternatural doorways said to open into other worlds, yet only @1 can I confirm as being more than merely stories.", nether.registered_portals_count)
  1482. end
  1483. -- tell the player how to ignite portals
  1484. local ignition_item_description = "<error - ignition item not set>"
  1485. if ignition_item_name ~= nil and minetest.registered_items[ignition_item_name] ~= nil then
  1486. ignition_item_description = minetest.registered_items[ignition_item_name].description
  1487. end
  1488. intro_text = intro_text ..
  1489. S("\n\nThe key to opening such a doorway is to strike the frame with a @1, at which point the very air inside begins to crackle and glow.", string.lower(ignition_item_description))
  1490. chapters[#chapters + 1] = {text = intro_text}
  1491. -- Describe how to create each type of portal, or perhaps just give clues or flavor text,
  1492. -- but ensure the Nether is always listed first on the first page so other definitions can
  1493. -- refer to it (pairs() returns order based on a random hash).
  1494. local portalDefs_in_order = {}
  1495. if nether.registered_portals["nether_portal"] then
  1496. portalDefs_in_order[#portalDefs_in_order + 1] = nether.registered_portals["nether_portal"]
  1497. end
  1498. for portalName, portalDef in pairs(nether.registered_portals) do
  1499. if portalName ~= "nether_portal" then
  1500. portalDefs_in_order[#portalDefs_in_order + 1] = portalDef
  1501. end
  1502. end
  1503. for _, portalDef in ipairs(portalDefs_in_order) do
  1504. chapters[#chapters + 1] = {
  1505. text = portalDef.book_of_portals_pagetext,
  1506. image = portalDef.shape.diagram_image,
  1507. title = portalDef.title
  1508. }
  1509. end
  1510. create_book(
  1511. ":nether:book_of_portals",
  1512. S("Book of Portals"),
  1513. "nether_book_of_portals.png",
  1514. S("A definitive guide to Rifts and Portals"),
  1515. "Riccard F. Burton", -- perhaps a Richard F. Burton of an alternate universe
  1516. chapters
  1517. )
  1518. local using_helpModpack = add_book_to_help_modpack(chapters)
  1519. if not using_helpModpack and not book_added_as_treasure and nether.PORTAL_BOOK_LOOT_WEIGHTING > 0 then
  1520. -- Only place the Book of Portals in chests if there are non-Nether (i.e. novel) portals
  1521. -- which players need a way to find out about.
  1522. if nether.registered_portals_count > 1 or (nether.registered_portals_count == 1 and nether.registered_portals["nether_portal"] == nil) then
  1523. add_book_as_treasure()
  1524. end
  1525. end
  1526. end
  1527. function register_frame_node(frame_node_name)
  1528. -- copy the existing node definition
  1529. local node_def = minetest.registered_nodes[frame_node_name]
  1530. local extended_node_def = {}
  1531. for key, value in pairs(node_def) do extended_node_def[key] = value end
  1532. extended_node_def.replaced_by_portalapi = {} -- allows chaining or restoration of original functions, if necessary
  1533. -- add portal portal functionality
  1534. extended_node_def.replaced_by_portalapi.mesecons = extended_node_def.mesecons
  1535. extended_node_def.mesecons = {effector = {
  1536. action_on = function (pos, node)
  1537. if DEBUG then minetest.chat_send_all("portal frame material: mesecons action ON") end
  1538. ignite_portal(pos, node.name)
  1539. end,
  1540. action_off = function (pos, node)
  1541. if DEBUG then minetest.chat_send_all("portal frame material: mesecons action OFF") end
  1542. extinguish_portal(pos, node.name, false)
  1543. end
  1544. }}
  1545. extended_node_def.replaced_by_portalapi.on_destruct = extended_node_def.on_destruct
  1546. extended_node_def.on_destruct = function(pos)
  1547. if DEBUG then minetest.chat_send_all("portal frame material: destruct") end
  1548. extinguish_portal(pos, frame_node_name, true)
  1549. end
  1550. extended_node_def.replaced_by_portalapi.on_blast = extended_node_def.on_blast
  1551. extended_node_def.on_blast = function(pos, intensity)
  1552. if DEBUG then minetest.chat_send_all("portal frame material: blast") end
  1553. extinguish_portal(pos, frame_node_name, extended_node_def.replaced_by_portalapi.on_blast == nil)
  1554. if extended_node_def.replaced_by_portalapi.on_blast ~= nil then
  1555. extended_node_def.replaced_by_portalapi.on_blast(pos, intensity)
  1556. else
  1557. minetest.remove_node(pos)
  1558. end
  1559. end
  1560. extended_node_def.replaced_by_portalapi.on_timer = extended_node_def.on_timer
  1561. extended_node_def.on_timer = function(pos, elapsed)
  1562. run_wormhole(pos, elapsed)
  1563. return true
  1564. end
  1565. -- replace the node with the new extended definition
  1566. minetest.register_node(":" .. frame_node_name, extended_node_def)
  1567. end
  1568. function unregister_frame_node(frame_node_name)
  1569. -- copy the existing node definition
  1570. local node = minetest.registered_nodes[frame_node_name]
  1571. local restored_node_def = {}
  1572. for key, value in pairs(node) do restored_node_def[key] = value end
  1573. -- remove portal portal functionality
  1574. restored_node_def.mesecons = nil
  1575. restored_node_def.on_destruct = nil
  1576. restored_node_def.on_timer = nil
  1577. restored_node_def.replaced_by_portalapi = nil
  1578. if node.replaced_by_portalapi ~= nil then
  1579. for key, value in pairs(node.replaced_by_portalapi) do restored_node_def[key] = value end
  1580. end
  1581. -- replace the node with the restored definition
  1582. minetest.register_node(":" .. frame_node_name, restored_node_def)
  1583. end
  1584. -- check for mistakes people might make in custom shape definitions
  1585. function test_shapedef_is_valid(shape_defintion)
  1586. assert(shape_defintion ~= nil, "shape definition cannot be nil")
  1587. assert(shape_defintion.name ~= nil, "shape definition must have a name")
  1588. local result = true
  1589. local origin = vector.new()
  1590. local p1, p2 = shape_defintion:get_p1_and_p2_from_anchorPos(origin, 0)
  1591. assert(vector.equals(shape_defintion.size, vector.add(vector.subtract(p2, p1), 1)), "p1 and p2 of shape definition '" .. shape_defintion.name .. "' don't match shapeDef.size")
  1592. assert(shape_defintion.diagram_image ~= nil and shape_defintion.diagram_image.image ~= nil, "Shape definition '" .. shape_defintion.name .. "' does not provide an image for Help/Book of Portals")
  1593. assert(shape_defintion.diagram_image.width > 0 and shape_defintion.diagram_image.height > 0, "Shape definition '" .. shape_defintion.name .. "' does not provide the size of the image for Help/Book of Portals")
  1594. -- todo
  1595. return result
  1596. end
  1597. -- check for mistakes people might make in portal definitions
  1598. function test_portaldef_is_valid(portal_definition)
  1599. local result = test_shapedef_is_valid(portal_definition.shape)
  1600. assert(portal_definition.wormhole_node_color >= 0 and portal_definition.wormhole_node_color < 8, "portaldef.wormhole_node_color must be between 0 and 7 (inclusive)")
  1601. assert(portal_definition.is_within_realm ~= nil, "portaldef.is_within_realm() must be implemented")
  1602. assert(portal_definition.find_realm_anchorPos ~= nil, "portaldef.find_realm_anchorPos() must be implemented")
  1603. if portal_definition.frame_node_color ~= nil then
  1604. assert(portal_definition.frame_node_color >= 0 and portal_definition.frame_node_color < 8, "portal_definition.frame_node_color must be between 0 and 7 (inclusive)")
  1605. end
  1606. -- todo
  1607. return result
  1608. end
  1609. -- convert portals made with old ABM version of nether mod to use the timer instead
  1610. minetest.register_lbm({
  1611. label = "Start portal timer",
  1612. name = "nether:start_portal_timer",
  1613. nodenames = {"nether:portal"},
  1614. run_at_every_load = false,
  1615. action = function(pos, node)
  1616. local p1, p2
  1617. local meta = minetest.get_meta(pos)
  1618. if meta ~= nil then
  1619. p1 = minetest.string_to_pos(meta:get_string("p1"))
  1620. p2 = minetest.string_to_pos(meta:get_string("p2"))
  1621. end
  1622. if p1 ~= nil and p2 ~= nil then
  1623. local timerPos = get_timerPos_from_p1_and_p2(p1, p2)
  1624. local timer = minetest.get_node_timer(timerPos)
  1625. if timer ~= nil then
  1626. timer:start(1)
  1627. if DEBUG then minetest.chat_send_all("LBM started portal timer " .. minetest.pos_to_string(timerPos)) end
  1628. elseif DEBUG then
  1629. minetest.chat_send_all("get_node_timer" .. minetest.pos_to_string(timerPos) .. " returned null")
  1630. end
  1631. end
  1632. end
  1633. })
  1634. -- Portal API functions --
  1635. -- ==================== --
  1636. -- the fallback defaults for wormhole nodedefs
  1637. local wormhole_nodedef_default = {
  1638. description = S("Portal wormhole"),
  1639. tiles = {
  1640. "nether_transparent.png",
  1641. "nether_transparent.png",
  1642. "nether_transparent.png",
  1643. "nether_transparent.png",
  1644. {
  1645. name = "nether_portal.png",
  1646. animation = {
  1647. type = "vertical_frames",
  1648. aspect_w = 16,
  1649. aspect_h = 16,
  1650. length = 0.9,
  1651. },
  1652. },
  1653. {
  1654. name = "nether_portal.png",
  1655. animation = {
  1656. type = "vertical_frames",
  1657. aspect_w = 16,
  1658. aspect_h = 16,
  1659. length = 0.9,
  1660. },
  1661. },
  1662. },
  1663. drawtype = "nodebox",
  1664. paramtype = "light",
  1665. paramtype2 = "colorfacedir",
  1666. palette = "nether_portals_palette.png",
  1667. post_effect_color = {
  1668. -- post_effect_color can't be changed dynamically in Minetest like the portal colour is.
  1669. -- If you need a different post_effect_color then use register_wormhole_node() to create
  1670. -- another wormhole node with the right post_effect_color and set it as the wormhole_node_name
  1671. -- in your portaldef.
  1672. -- Hopefully this colour is close enough to magenta to work with the traditional magenta
  1673. -- portals, close enough to red to work for a red portal, and also close enough to red to
  1674. -- work with blue & cyan portals - since blue portals are sometimes portrayed as being red
  1675. -- from the opposite side / from the inside.
  1676. a = 160, r = 128, g = 0, b = 80
  1677. },
  1678. sunlight_propagates = true,
  1679. use_texture_alpha = true,
  1680. walkable = false,
  1681. diggable = false,
  1682. pointable = false,
  1683. buildable_to = false,
  1684. is_ground_content = false,
  1685. drop = "",
  1686. light_source = 5,
  1687. alpha = 192,
  1688. node_box = {
  1689. type = "fixed",
  1690. fixed = {
  1691. {-0.5, -0.5, -0.1, 0.5, 0.5, 0.1},
  1692. },
  1693. },
  1694. groups = {not_in_creative_inventory = 1},
  1695. mesecons = {receptor = {
  1696. state = "on",
  1697. rules = function(node)
  1698. return nether.get_mesecon_emission_rules_from_colorfacedir(node.param2)
  1699. end
  1700. }}
  1701. }
  1702. -- Call only at load time
  1703. function nether.register_wormhole_node(name, nodedef)
  1704. assert(name ~= nil, "Unable to register wormhole node: Name is nil")
  1705. assert(nodedef ~= nil, "Unable to register wormhole node ''" .. name .. "'': nodedef is nil")
  1706. for key, value in pairs(wormhole_nodedef_default) do
  1707. if nodedef[key] == nil then nodedef[key] = value end
  1708. end
  1709. minetest.register_node(name, nodedef)
  1710. end
  1711. -- The fallback defaults for registered portaldef tables
  1712. local portaldef_default = {
  1713. title = S("Untitled portal"),
  1714. book_of_portals_pagetext = S("We know almost nothing about this portal"),
  1715. shape = nether.PortalShape_Traditional,
  1716. wormhole_node_name = "nether:portal",
  1717. wormhole_node_color = 0,
  1718. frame_node_name = "default:obsidian",
  1719. particle_texture = "nether_particle.png",
  1720. particle_texture_animation = nil,
  1721. particle_texture_scale = 1,
  1722. sounds = {
  1723. ambient = {name = "nether_portal_ambient", gain = 0.6, length = 3},
  1724. ignite = {name = "nether_portal_ignite", gain = 0.7},
  1725. extinguish = {name = "nether_portal_extinguish", gain = 0.6},
  1726. teleport = {name = "nether_portal_teleport", gain = 0.3}
  1727. }
  1728. }
  1729. function nether.register_portal(name, portaldef)
  1730. assert(name ~= nil, "Unable to register portal: Name is nil")
  1731. assert(portaldef ~= nil, "Unable to register portal ''" .. name .. "'': portaldef is nil")
  1732. if nether.registered_portals[name] ~= nil then
  1733. minetest.log("error", "Unable to register portal: '" .. name .. "' is already in use")
  1734. return false;
  1735. end
  1736. portaldef.name = name
  1737. portaldef.mod_name = minetest.get_current_modname()
  1738. -- use portaldef_default for any values missing from portaldef or portaldef.sounds
  1739. if portaldef.sounds ~= nil then setmetatable(portaldef.sounds, {__index = portaldef_default.sounds}) end
  1740. setmetatable(portaldef, {__index = portaldef_default})
  1741. if portaldef.particle_color == nil then
  1742. -- default the particle colours to be the same as the wormhole colour
  1743. assert(portaldef.wormhole_node_color >= 0 and portaldef.wormhole_node_color < 8, "portaldef.wormhole_node_color must be between 0 and 7 (inclusive)")
  1744. portaldef.particle_color = nether.portals_palette[portaldef.wormhole_node_color].asString
  1745. end
  1746. if portaldef.particle_texture_colored == nil then
  1747. -- Combine the particle texture with the particle color unless a particle_texture_colored was specified.
  1748. if type(portaldef.particle_texture) == "table" and portaldef.particle_texture.animation ~= nil then
  1749. portaldef.particle_texture_colored = portaldef.particle_texture.name .. "^[colorize:" .. portaldef.particle_color .. ":alpha"
  1750. portaldef.particle_texture_animation = portaldef.particle_texture.animation
  1751. portaldef.particle_texture_scale = portaldef.particle_texture.scale or 1
  1752. else
  1753. portaldef.particle_texture_colored = portaldef.particle_texture .. "^[colorize:" .. portaldef.particle_color .. ":alpha"
  1754. end
  1755. end
  1756. if portaldef.find_surface_anchorPos == nil then -- default to using find_surface_target_y()
  1757. portaldef.find_surface_anchorPos = function(pos)
  1758. local destination_pos = {x = pos.x, y = 0, z = pos.z}
  1759. local existing_portal_location, existing_portal_orientation =
  1760. nether.find_nearest_working_portal(name, destination_pos, 10, 0) -- a y_factor of 0 makes the search ignore the altitude of the portals (as long as they are outside the realm)
  1761. if existing_portal_location ~= nil then
  1762. return existing_portal_location, existing_portal_orientation
  1763. else
  1764. destination_pos.y = nether.find_surface_target_y(destination_pos.x, destination_pos.z, name)
  1765. return destination_pos
  1766. end
  1767. end
  1768. end
  1769. if test_portaldef_is_valid(portaldef) then
  1770. -- check whether the portal definition clashes with anyone else's portal
  1771. local p1, p2 = portaldef.shape:get_p1_and_p2_from_anchorPos(vector.new(), 0)
  1772. local existing_portaldef = get_portal_definition(portaldef.frame_node_name, p1, p2)
  1773. if existing_portaldef ~= nil then
  1774. minetest.log("error",
  1775. portaldef.mod_name .." tried to register a portal '" .. portaldef.name .. "' made of " .. portaldef.frame_node_name ..
  1776. ", but it is the same material and shape as the portal '" .. existing_portaldef.name .. "' already registered by " .. existing_portaldef.mod_name ..
  1777. ". Edit the values one of those mods uses in its call to nether.register_portal() if you wish to resolve this clash.")
  1778. else
  1779. -- the new portaldef is good
  1780. nether.registered_portals[portaldef.name] = portaldef
  1781. -- Update registered_portals_count
  1782. local portalCount = 0
  1783. for _ in pairs(nether.registered_portals) do portalCount = portalCount + 1 end
  1784. nether.registered_portals_count = portalCount
  1785. create_book_of_portals()
  1786. if not nether.is_frame_node[portaldef.frame_node_name] then
  1787. -- add portal functions to the nodedef being used for the portal frame
  1788. register_frame_node(portaldef.frame_node_name)
  1789. nether.is_frame_node[portaldef.frame_node_name] = true
  1790. end
  1791. return true
  1792. end
  1793. end
  1794. return false
  1795. end
  1796. function nether.unregister_portal(name)
  1797. assert(name ~= nil, "Cannot unregister portal: Name is nil")
  1798. local portaldef = nether.registered_portals[name]
  1799. local result = portaldef ~= nil
  1800. if portaldef ~= nil then
  1801. nether.registered_portals[name] = nil
  1802. local portals_still_using_frame_node = list_portal_definitions_for_frame_node(portaldef.frame_node_name)
  1803. if next(portals_still_using_frame_node) == nil then
  1804. -- no portals are using this frame node any more
  1805. unregister_frame_node(portaldef.frame_node_name)
  1806. nether.is_frame_node[portaldef.frame_node_name] = nil
  1807. end
  1808. end
  1809. return result
  1810. end
  1811. function nether.register_portal_ignition_item(item_name, ignition_failure_sound)
  1812. minetest.override_item(item_name, {
  1813. on_place = function(stack, _, pt)
  1814. local done = false
  1815. if pt.under and nether.is_frame_node[minetest.get_node(pt.under).name] then
  1816. done = ignite_portal(pt.under)
  1817. if done and not minetest.settings:get_bool("creative_mode") then
  1818. stack:take_item()
  1819. end
  1820. end
  1821. if not done and ignition_failure_sound ~= nil then
  1822. minetest.sound_play(ignition_failure_sound, {pos = pt.under, max_hear_distance = 10})
  1823. end
  1824. return stack
  1825. end,
  1826. })
  1827. ignition_item_name = item_name
  1828. end
  1829. -- use this when determining where to spawn a portal, to avoid overwriting player builds
  1830. -- It checks the area for any nodes that aren't ground or trees.
  1831. -- (Water also fails this test, unless it is unemerged)
  1832. function nether.volume_is_natural(minp, maxp)
  1833. local c_air = minetest.get_content_id("air")
  1834. local c_ignore = minetest.get_content_id("ignore")
  1835. local vm = minetest.get_voxel_manip()
  1836. local pos1 = {x = minp.x, y = minp.y, z = minp.z}
  1837. local pos2 = {x = maxp.x, y = maxp.y, z = maxp.z}
  1838. local emin, emax = vm:read_from_map(pos1, pos2)
  1839. local area = VoxelArea:new({MinEdge = emin, MaxEdge = emax})
  1840. local data = vm:get_data()
  1841. for z = pos1.z, pos2.z do
  1842. for y = pos1.y, pos2.y do
  1843. local vi = area:index(pos1.x, y, z)
  1844. for x = pos1.x, pos2.x do
  1845. local id = data[vi] -- Existing node
  1846. if DEBUG and id == nil then minetest.chat_send_all("nil block at index " .. vi) end
  1847. if id ~= c_air and id ~= c_ignore and id ~= nil then -- checked for common natural or not emerged
  1848. local name = minetest.get_name_from_content_id(id)
  1849. local nodedef = minetest.registered_nodes[name]
  1850. if not nodedef.is_ground_content then
  1851. -- trees are natural but not "ground content"
  1852. local node_groups = nodedef.groups
  1853. if node_groups == nil or (node_groups.tree == nil and node_groups.leaves == nil and node_groups.leafdecay == nil) then
  1854. if DEBUG then minetest.chat_send_all("volume_is_natural() found unnatural node " .. name) end
  1855. return false
  1856. end
  1857. end
  1858. end
  1859. vi = vi + 1
  1860. end
  1861. end
  1862. end
  1863. if DEBUG then minetest.chat_send_all("Volume is natural") end
  1864. return true
  1865. end
  1866. -- Can be used when implementing custom find_surface_anchorPos() functions
  1867. -- portal_name is optional, providing it allows existing portals on the surface to be reused.
  1868. function nether.find_surface_target_y(target_x, target_z, portal_name)
  1869. assert(target_x ~= nil and target_z ~= nil, "Arguments `target_x` and `target_z` cannot be nil when calling find_surface_target_y()")
  1870. -- default to starting the search at -16 (probably underground) if we don't know the
  1871. -- surface, like paramat's original code from before get_spawn_level() was available:
  1872. -- https://github.com/minetest-mods/nether/issues/5#issuecomment-506983676
  1873. local start_y = -16
  1874. -- try to spawn on surface first
  1875. if minetest.get_spawn_level ~= nil then -- older versions of Minetest don't have this
  1876. local surface_level = minetest.get_spawn_level(target_x, target_z)
  1877. if surface_level ~= nil then -- test this since get_spawn_level() can return nil over water or steep/high terrain
  1878. -- get_spawn_level() tends to err on the side of caution and spawns the player a
  1879. -- block higher than the ground level. The implementation is mapgen specific
  1880. -- and -2 seems to be the right correction for v6, v5, carpathian, valleys, and flat,
  1881. -- but v7 only needs -1.
  1882. -- Perhaps this was not always the case, and -2 may be too much in older versions
  1883. -- of minetest, but half-buried portals are perferable to floating ones, and they
  1884. -- will clear a suitable hole around themselves.
  1885. if minetest.get_mapgen_setting("mg_name") == "v7" then
  1886. surface_level = surface_level - 1
  1887. else
  1888. surface_level = surface_level - 2
  1889. end
  1890. start_y = surface_level
  1891. end
  1892. end
  1893. for y = start_y, start_y - 256, -16 do
  1894. -- Check volume for non-natural nodes
  1895. local minp = {x = target_x - 1, y = y - 1, z = target_z - 2}
  1896. local maxp = {x = target_x + 2, y = y + 3, z = target_z + 2}
  1897. if nether.volume_is_natural(minp, maxp) then
  1898. return y
  1899. elseif portal_name ~= nil and nether.registered_portals[portal_name] ~= nil then
  1900. -- players have built here - don't grief.
  1901. -- but reigniting existing portals in portal rooms is fine - desirable even.
  1902. local anchorPos, orientation, is_ignited = is_within_portal_frame(nether.registered_portals[portal_name], {x = target_x, y = y, z = target_z})
  1903. if anchorPos ~= nil then
  1904. if DEBUG then minetest.chat_send_all("Volume_is_natural check failed, but a portal frame is here " .. minetest.pos_to_string(anchorPos) .. ", so this is still a good target y level") end
  1905. return y
  1906. end
  1907. end
  1908. end
  1909. return start_y - 256 -- Fallback
  1910. end
  1911. -- Returns the anchorPos, orientation of the nearest portal, or nil.
  1912. -- A y_factor of 0 means y does not affect the distance_limit, a y_factor of 1 means y is included,
  1913. -- and a y_factor of 2 would squash the search-sphere by a factor of 2 on the y-axis, etc.
  1914. -- Pass a negative distance_limit to indicate no distance limit
  1915. function nether.find_nearest_working_portal(portal_name, anchorPos, distance_limit, y_factor)
  1916. local portal_definition = nether.registered_portals[portal_name]
  1917. assert(portal_definition ~= nil, "find_nearest_working_portal() called with portal_name '" .. portal_name .. "', but no portal is registered with that name.")
  1918. assert(anchorPos ~= nil, "Argument `anchorPos` cannot be nil when calling find_nearest_working_portal()")
  1919. local contenders = list_closest_portals(portal_definition, anchorPos, distance_limit, y_factor)
  1920. -- sort by distance
  1921. local dist_list = {}
  1922. for dist, _ in pairs(contenders) do table.insert(dist_list, dist) end
  1923. table.sort(dist_list)
  1924. for _, dist in ipairs(dist_list) do
  1925. local portal_info = contenders[dist]
  1926. if DEBUG then minetest.chat_send_all("checking portal from mod_storage at " .. minetest.pos_to_string(portal_info.anchorPos) .. " orientation " .. portal_info.orientation) end
  1927. -- the mod_storage list of portals is unreliable - e.g. it won't know if inactive portals have been
  1928. -- destroyed, so check the portal is still there
  1929. local portalFound, portalIsActive = is_portal_at_anchorPos(portal_definition, portal_info.anchorPos, portal_info.orientation, true)
  1930. if portalFound then
  1931. return portal_info.anchorPos, portal_info.orientation
  1932. else
  1933. if DEBUG then minetest.chat_send_all("Portal wasn't found, removing portal from mod_storage at " .. minetest.pos_to_string(portal_info.anchorPos) .. " orientation " .. portal_info.orientation) end
  1934. -- The portal at that location must have been destroyed
  1935. remove_portal_location_info(portal_name, portal_info.anchorPos)
  1936. end
  1937. end
  1938. return nil
  1939. end