1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305 |
- --[[
- Portal API for Minetest
- See portal_api.txt for documentation
- --
- Copyright (C) 2020 Treer
- Permission to use, copy, modify, and/or distribute this software for
- any purpose with or without fee is hereby granted, provided that the
- above copyright notice and this permission notice appear in all copies.
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR
- BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES
- OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
- WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
- ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
- SOFTWARE.
- ]]--
- local DEBUG = false
- 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
- nether.registered_portals = {}
- nether.registered_portals_count = 0
- -- Exposes a list of node names that are used as frame nodes by registered portals
- nether.is_frame_node = {}
- -- gives the colour values in nether_portals_palette.png that are used by the wormhole colorfacedir
- -- hardware colouring.
- nether.portals_palette = {
- [0] = {r = 128, g = 0, b = 128, asString = "#800080"}, -- traditional/magenta
- [1] = {r = 0, g = 0, b = 0, asString = "#000000"}, -- black
- [2] = {r = 19, g = 19, b = 255, asString = "#1313FF"}, -- blue
- [3] = {r = 55, g = 168, b = 0, asString = "#37A800"}, -- green
- [4] = {r = 141, g = 237, b = 255, asString = "#8DEDFF"}, -- cyan
- [5] = {r = 221, g = 0, b = 0, asString = "#DD0000"}, -- red
- [6] = {r = 255, g = 240, b = 0, asString = "#FFF000"}, -- yellow
- [7] = {r = 255, g = 255, b = 255, asString = "#FFFFFF"} -- white
- }
- if minetest.get_mod_storage == nil then
- error(nether.modname .. " does not support Minetest versions earlier than 0.4.16", 0)
- end
- --[[
- Positions
- =========
- p1 & p2 p1 and p2 is the system used by earlier versions of the nether mod, which the portal_api
- is forwards and backwards compatible with.
- p1 is the bottom/west/south corner of the portal, and p2 is the opposite corner, together
- they define the bounding volume for the portal.
- The value of p1 and p2 is kept in the metadata of every node in the portal
- WormholePos The location of the node that a portal's target is set to, and a player is teleported
- to. It can also be used to test whether a portal is active.
- AnchorPos Introduced by the portal_api. Coordinates for portals are normally given in terms of
- the AnchorPos. The AnchorPos does not change with portal orientation - portals rotate
- around the AnchorPos. Ideally an AnchorPos would be near the bottom center of a portal
- shape, but this is not the case with PortalShape_Traditional to keep comptaibility with
- earlier versions of the nether mod.
- Usually an orientation is required with an AnchorPos.
- Orientation is yaw, either 0 or 90, 0 meaning a portal that faces north/south - i.e. obsidian
- running east/west.
- TimerPos The portal_api replaces ABMs with a single node timer per portal, and the TimerPos is the
- node in which that timer is located. Extra metadata is also kept in the TimerPos node.
- Portal shapes
- =============
- For the PortalShape_Traditional implementation, p1, p2, anchorPos, wormholdPos and TimerPos are defined
- as follows:
- .
- +--------+--------+--------+--------+
- | | Frame | |
- | | | | p2 |
- +--------+--------+--------+--------+
- | | | |
- | | | |
- +--------+ + +--------+
- | | Wormhole | |
- | | | |
- +--------+ + +--------+
- | |Wormhole | |
- | | Pos | |
- +--------+--------+--------+--------+
- AnchorPos|TimerPos| | |
- | p1 | | | |
- +--------+--------+--------+--------+
- +X/East or +Z/North ----->
- A better location for AnchorPos would be directly under WormholePos, as it's more centered
- and you don't need to know the portal's orientation to find AnchorPos from the WormholePos
- or vice-versa, however AnchorPos is in the bottom/south/west-corner to keep compatibility
- with earlier versions of nether mod (which only records portal corners p1 & p2 in the node
- metadata).
- ]]
- local facedir_up, facedir_north, facedir_south, facedir_east, facedir_west, facedir_down = 0, 4, 8, 12, 16, 20
- local __ = {name = "air", prob = 0}
- local AA = {name = "air", prob = 255, force_place = true}
- local ON = {name = "default:obsidian", facedir = facedir_north + 0, prob = 255, force_place = true}
- local ON2 = {name = "default:obsidian", facedir = facedir_north + 1, prob = 255, force_place = true}
- local ON3 = {name = "default:obsidian", facedir = facedir_north + 2, prob = 255, force_place = true}
- local ON4 = {name = "default:obsidian", facedir = facedir_north + 3, prob = 255, force_place = true}
- local OS = {name = "default:obsidian", facedir = facedir_south, prob = 255, force_place = true}
- local OE = {name = "default:obsidian", facedir = facedir_east, prob = 255, force_place = true}
- local OW = {name = "default:obsidian", facedir = facedir_west, prob = 255, force_place = true}
- local OU = {name = "default:obsidian", facedir = facedir_up + 0, prob = 255, force_place = true}
- local OU2 = {name = "default:obsidian", facedir = facedir_up + 1, prob = 255, force_place = true}
- local OU3 = {name = "default:obsidian", facedir = facedir_up + 2, prob = 255, force_place = true}
- local OU4 = {name = "default:obsidian", facedir = facedir_up + 3, prob = 255, force_place = true}
- local OD = {name = "default:obsidian", facedir = facedir_down, prob = 255, force_place = true}
- -- facedirNodeList is a list of node references which should have their facedir value copied into
- -- param2 before placing a schematic. The facedir values will only be copied when the portal's frame
- -- node has a paramtype2 of "facedir" or "colorfacedir".
- -- Having schematics provide this list avoids needing to check every node in the schematic volume.
- local facedirNodeList = {ON, ON2, ON3, ON4, OS, OE, OW, OU, OU2, OU3, OU4, OD}
- -- This object defines a portal's shape, segregating the shape logic code from portal behaviour code.
- -- You can create a new "PortalShape" definition object which implements the same
- -- functions if you wish to register a custom shaped portal in register_portal(). Examples of other
- -- shapes follow after PortalShape_Traditional.
- -- Since it's symmetric, this PortalShape definition has only implemented orientations of 0 and 90
- nether.PortalShape_Traditional = {
- name = "Traditional",
- size = vector.new(4, 5, 1), -- size of the portal, and not necessarily the size of the schematic,
- -- which may clear area around the portal.
- is_horizontal = false, -- whether the wormhole is a vertical or horizontal surface
- diagram_image = {
- image = "nether_book_diagram_traditional.png", -- The diagram to be shown in the Book of Portals
- width = 142,
- height = 305
- },
- -- returns the coords for minetest.place_schematic() that will place the schematic on the anchorPos
- get_schematicPos_from_anchorPos = function(anchorPos, orientation)
- assert(orientation, "no orientation passed")
- if orientation == 0 then
- return {x = anchorPos.x, y = anchorPos.y, z = anchorPos.z - 2}
- else
- return {x = anchorPos.x - 2, y = anchorPos.y, z = anchorPos.z }
- end
- end,
- get_wormholePos_from_anchorPos = function(anchorPos, orientation)
- assert(orientation, "no orientation passed")
- if orientation == 0 then
- return {x = anchorPos.x + 1, y = anchorPos.y + 1, z = anchorPos.z }
- else
- return {x = anchorPos.x, y = anchorPos.y + 1, z = anchorPos.z + 1}
- end
- end,
- get_anchorPos_from_wormholePos = function(wormholePos, orientation)
- assert(orientation, "no orientation passed")
- if orientation == 0 then
- return {x = wormholePos.x - 1, y = wormholePos.y - 1, z = wormholePos.z }
- else
- return {x = wormholePos.x, y = wormholePos.y - 1, z = wormholePos.z - 1}
- end
- end,
- -- p1 and p2 are used to keep maps compatible with earlier versions of this mod.
- -- p1 is the bottom/west/south corner of the portal, and p2 is the opposite corner, together
- -- they define the bounding volume for the portal.
- get_p1_and_p2_from_anchorPos = function(self, anchorPos, orientation)
- assert(orientation, "no orientation passed")
- assert(self ~= nil and self.name == nether.PortalShape_Traditional.name, "Must pass self as first argument, or use shape:func() instead of shape.func()")
- local p1 = anchorPos -- PortalShape_Traditional puts the anchorPos at p1 for backwards&forwards compatibility
- local p2
- if orientation == 0 then
- p2 = {x = p1.x + self.size.x - 1, y = p1.y + self.size.y - 1, z = p1.z }
- else
- p2 = {x = p1.x, y = p1.y + self.size.y - 1, z = p1.z + self.size.x - 1}
- end
- return p1, p2
- end,
- get_anchorPos_and_orientation_from_p1_and_p2 = function(p1, p2)
- if p1.z == p2.z then
- return p1, 0
- elseif p1.x == p2.x then
- return p1, 90
- else
- -- this KISS implementation will break you've made a 3D PortalShape definition
- 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))
- end
- end,
- -- returns true if function was applied to all frame nodes
- apply_func_to_frame_nodes = function(anchorPos, orientation, func)
- -- a 4x5 portal is small enough that hardcoded positions is simpler that procedural code
- local shortCircuited
- if orientation == 0 then
- -- use short-circuiting of boolean evaluation to allow func() to cause an abort by returning true
- shortCircuited =
- func({x = anchorPos.x + 0, y = anchorPos.y, z = anchorPos.z}) or
- func({x = anchorPos.x + 1, y = anchorPos.y, z = anchorPos.z}) or
- func({x = anchorPos.x + 2, y = anchorPos.y, z = anchorPos.z}) or
- func({x = anchorPos.x + 3, y = anchorPos.y, z = anchorPos.z}) or
- func({x = anchorPos.x + 0, y = anchorPos.y + 4, z = anchorPos.z}) or
- func({x = anchorPos.x + 1, y = anchorPos.y + 4, z = anchorPos.z}) or
- func({x = anchorPos.x + 2, y = anchorPos.y + 4, z = anchorPos.z}) or
- func({x = anchorPos.x + 3, y = anchorPos.y + 4, z = anchorPos.z}) or
- func({x = anchorPos.x, y = anchorPos.y + 1, z = anchorPos.z}) or
- func({x = anchorPos.x, y = anchorPos.y + 2, z = anchorPos.z}) or
- func({x = anchorPos.x, y = anchorPos.y + 3, z = anchorPos.z}) or
- func({x = anchorPos.x + 3, y = anchorPos.y + 1, z = anchorPos.z}) or
- func({x = anchorPos.x + 3, y = anchorPos.y + 2, z = anchorPos.z}) or
- func({x = anchorPos.x + 3, y = anchorPos.y + 3, z = anchorPos.z})
- else
- shortCircuited =
- func({x = anchorPos.x, y = anchorPos.y, z = anchorPos.z + 0}) or
- func({x = anchorPos.x, y = anchorPos.y, z = anchorPos.z + 1}) or
- func({x = anchorPos.x, y = anchorPos.y, z = anchorPos.z + 2}) or
- func({x = anchorPos.x, y = anchorPos.y, z = anchorPos.z + 3}) or
- func({x = anchorPos.x, y = anchorPos.y + 4, z = anchorPos.z + 0}) or
- func({x = anchorPos.x, y = anchorPos.y + 4, z = anchorPos.z + 1}) or
- func({x = anchorPos.x, y = anchorPos.y + 4, z = anchorPos.z + 2}) or
- func({x = anchorPos.x, y = anchorPos.y + 4, z = anchorPos.z + 3}) or
- func({x = anchorPos.x, y = anchorPos.y + 1, z = anchorPos.z }) or
- func({x = anchorPos.x, y = anchorPos.y + 2, z = anchorPos.z }) or
- func({x = anchorPos.x, y = anchorPos.y + 3, z = anchorPos.z }) or
- func({x = anchorPos.x, y = anchorPos.y + 1, z = anchorPos.z + 3}) or
- func({x = anchorPos.x, y = anchorPos.y + 2, z = anchorPos.z + 3}) or
- func({x = anchorPos.x, y = anchorPos.y + 3, z = anchorPos.z + 3})
- end
- return not shortCircuited
- end,
- -- returns true if function was applied to all wormhole nodes
- apply_func_to_wormhole_nodes = function(anchorPos, orientation, func)
- local shortCircuited
- if orientation == 0 then
- local wormholePos = {x = anchorPos.x + 1, y = anchorPos.y + 1, z = anchorPos.z}
- -- use short-circuiting of boolean evaluation to allow func() to cause an abort by returning true
- shortCircuited =
- func({x = wormholePos.x + 0, y = wormholePos.y + 0, z = wormholePos.z}) or
- func({x = wormholePos.x + 1, y = wormholePos.y + 0, z = wormholePos.z}) or
- func({x = wormholePos.x + 0, y = wormholePos.y + 1, z = wormholePos.z}) or
- func({x = wormholePos.x + 1, y = wormholePos.y + 1, z = wormholePos.z}) or
- func({x = wormholePos.x + 0, y = wormholePos.y + 2, z = wormholePos.z}) or
- func({x = wormholePos.x + 1, y = wormholePos.y + 2, z = wormholePos.z})
- else
- local wormholePos = {x = anchorPos.x, y = anchorPos.y + 1, z = anchorPos.z + 1}
- shortCircuited =
- func({x = wormholePos.x, y = wormholePos.y + 0, z = wormholePos.z + 0}) or
- func({x = wormholePos.x, y = wormholePos.y + 0, z = wormholePos.z + 1}) or
- func({x = wormholePos.x, y = wormholePos.y + 1, z = wormholePos.z + 0}) or
- func({x = wormholePos.x, y = wormholePos.y + 1, z = wormholePos.z + 1}) or
- func({x = wormholePos.x, y = wormholePos.y + 2, z = wormholePos.z + 0}) or
- func({x = wormholePos.x, y = wormholePos.y + 2, z = wormholePos.z + 1})
- end
- return not shortCircuited
- end,
- -- Check for whether the portal is blocked in, and if so then provide a safe way
- -- on one side for the player to step out of the portal. Suggest including a roof
- -- incase the portal was blocked with lava flowing from above.
- -- If portal can appear in mid-air then can also check for that and add a platform.
- disable_portal_trap = function(anchorPos, orientation)
- assert(orientation, "no orientation passed")
- -- Not implemented yet. It may not need to be implemented because if you
- -- wait in a portal long enough you teleport again. So a trap portal would have to link
- -- to one of two blocked-in portals which link to each other - which is possible, but
- -- quite extreme.
- end,
- schematic = {
- size = {x = 4, y = 5, z = 5},
- data = { -- note that data is upside down
- __,__,__,__,
- AA,AA,AA,AA,
- AA,AA,AA,AA,
- AA,AA,AA,AA,
- AA,AA,AA,AA,
- __,__,__,__,
- AA,AA,AA,AA,
- AA,AA,AA,AA,
- AA,AA,AA,AA,
- AA,AA,AA,AA,
- ON,OW,OE,ON2,
- OU,AA,AA,OU,
- OU,AA,AA,OU,
- OU,AA,AA,OU,
- ON4,OE,OW,ON3,
- __,__,__,__,
- AA,AA,AA,AA,
- AA,AA,AA,AA,
- AA,AA,AA,AA,
- AA,AA,AA,AA,
- __,__,__,__,
- AA,AA,AA,AA,
- AA,AA,AA,AA,
- AA,AA,AA,AA,
- AA,AA,AA,AA,
- },
- facedirNodes = facedirNodeList
- }
- } -- End of PortalShape_Traditional class
- -- Example alternative PortalShape
- nether.PortalShape_Circular = {
- name = "Circular",
- size = vector.new(7, 7, 1), -- size of the portal, and not necessarily the size of the schematic,
- -- which may clear area around the portal.
- is_horizontal = false, -- whether the wormhole is a vertical or horizontal surface
- diagram_image = {
- image = "nether_book_diagram_circular.png", -- The diagram to be shown in the Book of Portals
- width = 149,
- height = 243
- },
- -- returns the coords for minetest.place_schematic() that will place the schematic on the anchorPos
- get_schematicPos_from_anchorPos = function(anchorPos, orientation)
- assert(orientation, "no orientation passed")
- if orientation == 0 then
- return {x = anchorPos.x - 3, y = anchorPos.y, z = anchorPos.z - 3}
- else
- return {x = anchorPos.x - 3, y = anchorPos.y, z = anchorPos.z - 3 }
- end
- end,
- get_wormholePos_from_anchorPos = function(anchorPos, orientation)
- -- wormholePos is the node above anchorPos
- return {x = anchorPos.x, y = anchorPos.y + 1, z = anchorPos.z}
- end,
- get_anchorPos_from_wormholePos = function(wormholePos, orientation)
- -- wormholePos is the node above anchorPos
- return {x = wormholePos.x, y = wormholePos.y - 1, z = wormholePos.z}
- end,
- -- p1 and p2 are used to keep maps compatible with earlier versions of this mod.
- -- p1 is the bottom/west/south corner of the portal, and p2 is the opposite corner, together
- -- they define the bounding volume for the portal.
- get_p1_and_p2_from_anchorPos = function(self, anchorPos, orientation)
- assert(orientation, "no orientation passed")
- assert(self ~= nil and self.name == nether.PortalShape_Circular.name, "Must pass self as first argument, or use shape:func() instead of shape.func()")
- local p1
- local p2
- if orientation == 0 then
- p1 = {x = anchorPos.x - 3, y = anchorPos.y, z = anchorPos.z }
- p2 = {x = p1.x + self.size.x - 1, y = p1.y + self.size.y - 1, z = p1.z }
- else
- p1 = {x = anchorPos.x, y = anchorPos.y, z = anchorPos.z - 3 }
- p2 = {x = p1.x, y = p1.y + self.size.y - 1, z = p1.z + self.size.x - 1}
- end
- return p1, p2
- end,
- get_anchorPos_and_orientation_from_p1_and_p2 = function(p1, p2)
- if p1.z == p2.z then
- return {x= p1.x + 3, y = p1.y, z = p1.z }, 0
- elseif p1.x == p2.x then
- return {x= p1.x, y = p1.y, z = p1.z + 3}, 90
- end
- end,
- apply_func_to_frame_nodes = function(anchorPos, orientation, func)
- local shortCircuited
- if orientation == 0 then
- -- use short-circuiting of boolean evaluation to allow func() to cause an abort by returning true
- shortCircuited =
- func({x = anchorPos.x + 0, y = anchorPos.y + 0, z = anchorPos.z}) or
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- func({x = anchorPos.x + 0, y = anchorPos.y + 6, z = anchorPos.z})
- else
- shortCircuited =
- func({x = anchorPos.x, y = anchorPos.y + 0, z = anchorPos.z + 0}) or
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- func({x = anchorPos.x, y = anchorPos.y + 6, z = anchorPos.z + 0})
- end
- return not shortCircuited
- end,
- -- returns true if function was applied to all wormhole nodes
- apply_func_to_wormhole_nodes = function(anchorPos, orientation, func)
- local xRange = 2
- local zRange = 0
- if orientation ~= 0 then
- xRange = 0
- zRange = 2
- end
- local xEdge, yEdge, zEdge
- local pos = {}
- for x = -xRange, xRange do
- pos.x = anchorPos.x + x
- xEdge = x == -xRange or x == xRange
- for z = -zRange, zRange do
- zEdge = z == -zRange or z == zRange
- pos.z = anchorPos.z + z
- for y = 1, 5 do
- yEdge = y == 1 or y == 5
- if not (yEdge and xEdge and zEdge) then
- pos.y = anchorPos.y + y
- if func(pos) then
- -- func() caused an abort by returning true
- return false
- end
- end
- end
- end
- end
- return true
- end,
- -- Check for whether the portal is blocked in, and if so then provide a safe way
- -- on one side for the player to step out of the portal. Suggest including a roof
- -- incase the portal was blocked with lava flowing from above.
- -- If portal can appear in mid-air then can also check for that and add a platform.
- disable_portal_trap = function(anchorPos, orientation)
- assert(orientation, "no orientation passed")
- -- Not implemented.
- end,
- schematic = {
- size = {x = 7, y = 7, z = 7},
- data = { -- note that data is upside down
- __,__,__,__,__,__,__,
- __,__,__,__,__,__,__,
- __,__,AA,AA,AA,__,__,
- __,__,AA,AA,AA,__,__,
- __,__,AA,AA,AA,__,__,
- __,__,__,__,__,__,__,
- __,__,__,__,__,__,__,
- __,__,__,__,__,__,__,
- __,AA,AA,AA,AA,AA,__,
- __,AA,AA,AA,AA,AA,__,
- __,AA,AA,AA,AA,AA,__,
- __,AA,AA,AA,AA,AA,__,
- __,AA,AA,AA,AA,AA,__,
- __,__,__,__,__,__,__,
- __,__,__,__,__,__,__,
- __,AA,AA,AA,AA,AA,__,
- AA,AA,AA,AA,AA,AA,AA,
- AA,AA,AA,AA,AA,AA,AA,
- AA,AA,AA,AA,AA,AA,AA,
- __,AA,AA,AA,AA,AA,__,
- __,__,AA,AA,AA,__,__,
- __,__,OW,OW,OW,__,__,
- __,ON,AA,AA,AA,ON2,__,
- OU,AA,AA,AA,AA,AA,OD,
- OU,AA,AA,AA,AA,AA,OD,
- OU,AA,AA,AA,AA,AA,OD,
- __,ON4,AA,AA,AA,ON3,__,
- __,__,OE,OE,OE,__,__,
- __,__,__,__,__,__,__,
- __,AA,AA,AA,AA,AA,__,
- AA,AA,AA,AA,AA,AA,AA,
- AA,AA,AA,AA,AA,AA,AA,
- AA,AA,AA,AA,AA,AA,AA,
- __,AA,AA,AA,AA,AA,__,
- __,__,AA,AA,AA,__,__,
- __,__,__,__,__,__,__,
- __,AA,AA,AA,AA,AA,__,
- __,AA,AA,AA,AA,AA,__,
- __,AA,AA,AA,AA,AA,__,
- __,AA,AA,AA,AA,AA,__,
- __,AA,AA,AA,AA,AA,__,
- __,__,__,__,__,__,__,
- __,__,__,__,__,__,__,
- __,__,__,__,__,__,__,
- __,__,AA,AA,AA,__,__,
- __,__,AA,AA,AA,__,__,
- __,__,AA,AA,AA,__,__,
- __,__,__,__,__,__,__,
- __,__,__,__,__,__,__,
- },
- facedirNodes = facedirNodeList
- }
- } -- End of PortalShape_Circular class
- -- Example alternative PortalShape
- -- This platform shape is symmetrical around the y-axis, so the orientation value never matters.
- nether.PortalShape_Platform = {
- name = "Platform",
- size = vector.new(5, 2, 5), -- size of the portal, and not necessarily the size of the schematic,
- -- which may clear area around the portal.
- is_horizontal = true, -- whether the wormhole is a vertical or horizontal surface
- diagram_image = {
- image = "nether_book_diagram_platform.png", -- The diagram to be shown in the Book of Portals
- width = 200,
- height = 130
- },
- -- returns the coords for minetest.place_schematic() that will place the schematic on the anchorPos
- get_schematicPos_from_anchorPos = function(anchorPos, orientation)
- return {x = anchorPos.x - 2, y = anchorPos.y, z = anchorPos.z - 2}
- end,
- get_wormholePos_from_anchorPos = function(anchorPos, orientation)
- -- wormholePos is the node above anchorPos
- return {x = anchorPos.x, y = anchorPos.y + 1, z = anchorPos.z}
- end,
- get_anchorPos_from_wormholePos = function(wormholePos, orientation)
- -- wormholePos is the node above anchorPos
- return {x = wormholePos.x, y = wormholePos.y - 1, z = wormholePos.z}
- end,
- -- p1 and p2 are used to keep maps compatible with earlier versions of this mod.
- -- p1 is the bottom/west/south corner of the portal, and p2 is the opposite corner, together
- -- they define the bounding volume for the portal.
- get_p1_and_p2_from_anchorPos = function(self, anchorPos, orientation)
- assert(self ~= nil and self.name == nether.PortalShape_Platform.name, "Must pass self as first argument, or use shape:func() instead of shape.func()")
- local p1 = {x = anchorPos.x - 2, y = anchorPos.y, z = anchorPos.z - 2}
- local p2 = {x = anchorPos.x + 2, y = anchorPos.y + 1, z = anchorPos.z + 2}
- return p1, p2
- end,
- get_anchorPos_and_orientation_from_p1_and_p2 = function(p1, p2)
- return {x= p1.x + 2, y = p1.y, z = p1.z + 2}, 0
- end,
- apply_func_to_frame_nodes = function(anchorPos, orientation, func)
- local shortCircuited
- local yPlus1 = anchorPos.y + 1
- -- use short-circuiting of boolean evaluation to allow func() to cause an abort by returning true
- shortCircuited =
- func({x = anchorPos.x - 2, y = yPlus1, z = anchorPos.z - 1}) or func({x = anchorPos.x + 2, y = yPlus1, z = anchorPos.z - 1}) or
- func({x = anchorPos.x - 2, y = yPlus1, z = anchorPos.z }) or func({x = anchorPos.x + 2, y = yPlus1, z = anchorPos.z }) or
- func({x = anchorPos.x - 2, y = yPlus1, z = anchorPos.z + 1}) or func({x = anchorPos.x + 2, y = yPlus1, z = anchorPos.z + 1}) or
- func({x = anchorPos.x - 1, y = yPlus1, z = anchorPos.z - 2}) or func({x = anchorPos.x - 1, y = yPlus1, z = anchorPos.z + 2}) or
- func({x = anchorPos.x , y = yPlus1, z = anchorPos.z - 2}) or func({x = anchorPos.x , y = yPlus1, z = anchorPos.z + 2}) or
- func({x = anchorPos.x + 1, y = yPlus1, z = anchorPos.z - 2}) or func({x = anchorPos.x + 1, y = yPlus1, z = anchorPos.z + 2}) or
- func({x = anchorPos.x - 1, y = anchorPos.y, z = anchorPos.z - 1}) or
- func({x = anchorPos.x - 1, y = anchorPos.y, z = anchorPos.z }) or
- func({x = anchorPos.x - 1, y = anchorPos.y, z = anchorPos.z + 1}) or
- func({x = anchorPos.x , y = anchorPos.y, z = anchorPos.z - 1}) or
- func({x = anchorPos.x , y = anchorPos.y, z = anchorPos.z }) or
- func({x = anchorPos.x , y = anchorPos.y, z = anchorPos.z + 1}) or
- func({x = anchorPos.x + 1, y = anchorPos.y, z = anchorPos.z - 1}) or
- func({x = anchorPos.x + 1, y = anchorPos.y, z = anchorPos.z }) or
- func({x = anchorPos.x + 1, y = anchorPos.y, z = anchorPos.z + 1})
- return not shortCircuited
- end,
- -- returns true if function was applied to all wormhole nodes
- apply_func_to_wormhole_nodes = function(anchorPos, orientation, func)
- local shortCircuited
- local yPlus1 = anchorPos.y + 1
- -- use short-circuiting of boolean evaluation to allow func() to cause an abort by returning true
- shortCircuited =
- func({x = anchorPos.x - 1, y = yPlus1, z = anchorPos.z - 1}) or
- func({x = anchorPos.x - 1, y = yPlus1, z = anchorPos.z }) or
- func({x = anchorPos.x - 1, y = yPlus1, z = anchorPos.z + 1}) or
- func({x = anchorPos.x , y = yPlus1, z = anchorPos.z - 1}) or
- func({x = anchorPos.x , y = yPlus1, z = anchorPos.z }) or
- func({x = anchorPos.x , y = yPlus1, z = anchorPos.z + 1}) or
- func({x = anchorPos.x + 1, y = yPlus1, z = anchorPos.z - 1}) or
- func({x = anchorPos.x + 1, y = yPlus1, z = anchorPos.z }) or
- func({x = anchorPos.x + 1, y = yPlus1, z = anchorPos.z + 1})
- return not shortCircuited
- end,
- -- Check for suffocation
- disable_portal_trap = function(anchorPos, orientation)
- -- Not implemented.
- end,
- schematic = {
- size = {x = 5, y = 5, z = 5},
- data = { -- note that data is upside down
- __,__,__,__,__,
- OU4,OW,OW,OW,OU3,
- __,AA,AA,AA,__,
- __,AA,AA,AA,__,
- __,__,__,__,__,
- __,OU4,OW,OU3,__,
- ON,AA,AA,AA,OS,
- AA,AA,AA,AA,AA,
- AA,AA,AA,AA,AA,
- __,AA,AA,AA,__,
- __,ON,OD,OS,__,
- ON,AA,AA,AA,OS,
- AA,AA,AA,AA,AA,
- AA,AA,AA,AA,AA,
- __,AA,AA,AA,__,
- __,OU,OE,OU2,__,
- ON,AA,AA,AA,OS,
- AA,AA,AA,AA,AA,
- AA,AA,AA,AA,AA,
- __,AA,AA,AA,__,
- __,__,__,__,__,
- OU,OE,OE,OE,OU2,
- __,AA,AA,AA,__,
- __,AA,AA,AA,__,
- __,__,__,__,__,
- },
- facedirNodes = facedirNodeList
- }
- } -- End of PortalShape_Platform class
- --====================================================--
- --======== End of PortalShape implementations ========--
- --====================================================--
- -- Portal implementation functions --
- -- =============================== --
- local ignition_item_name
- local S = nether.get_translator
- local mod_storage = minetest.get_mod_storage()
- local meseconsAvailable = minetest.get_modpath("mesecon") ~= nil and minetest.global_exists("mesecon")
- local book_added_as_treasure = false
- local function get_timerPos_from_p1_and_p2(p1, p2)
- -- Pick a frame node for the portal's timer.
- --
- -- The timer event will need to know the portal definition, which can be determined by
- -- what the portal frame is made from, so the timer node should be on the frame.
- -- The timer event will also need to know its portal orientation, but unless someone
- -- makes a cubic portal shape, orientation can be determined from p1 and p2 in the node's
- -- metadata (frame nodes don't have orientation set in param2 like wormhole nodes do).
- --
- -- We shouldn't pick p1 or p2 as it's possible for two orthogonal portals to share
- -- the same p1, etc. - or at least it was - there's code to try to stop that now.
- --
- -- I'll pick the bottom center node of the portal, since that works for rectangular portals
- -- and if someone want to make a circular portal then that positon will still likely be part
- -- of the frame.
- return {
- x = math.floor((p1.x + p2.x) / 2),
- y = p1.y,
- z = math.floor((p1.z + p2.z) / 2),
- }
- end
- -- orientation is the yaw rotation degrees passed to place_schematic: 0, 90, 180, or 270
- -- color is a value from 0 to 7 corresponding to the color of pixels in nether_portals_palette.png
- -- portal_is_horizontal is a bool indicating whether the portal lies flat or stands vertically
- local function get_colorfacedir_from_color_and_orientation(color, orientation, portal_is_horizontal)
- assert(orientation, "no orientation passed")
- local axis_direction, rotation
- local dir = math.floor((orientation % 360) / 90 + 0.5)
- -- if the portal is vertical then node axis direction will be +Y (up) and portal orientation
- -- will set the node's rotation.
- -- if the portal is horizontal then the node axis direction reflects the yaw orientation and
- -- the node's rotation will be whatever's needed to keep the texture horizontal (either 0 or 1)
- if portal_is_horizontal then
- if dir == 0 then axis_direction = 1 end -- North
- if dir == 1 then axis_direction = 3 end -- East
- if dir == 2 then axis_direction = 2 end -- South
- if dir == 3 then axis_direction = 4 end -- West
- rotation = math.floor(axis_direction / 2); -- a rotation is only needed if axis_direction is east or west
- else
- axis_direction = 0 -- 0 is up, or +Y
- rotation = dir
- end
- -- wormhole nodes have a paramtype2 of colorfacedir, which means the
- -- high 3 bits are palette, followed by 3 direction bits and 2 rotation bits.
- -- We set the palette bits and rotation
- return rotation + axis_direction * 4 + color * 32
- end
- local function get_orientation_from_colorfacedir(param2)
- local axis_direction = 0
- -- Strip off the top 6 bits to leave the 2 rotation bits, unfortunately MT lua has no bitwise '&'
- -- (high 3 bits are palette, followed by 3 direction bits then 2 rotation bits)
- if param2 >= 128 then param2 = param2 - 128 end
- if param2 >= 64 then param2 = param2 - 64 end
- if param2 >= 32 then param2 = param2 - 32 end
- if param2 >= 16 then param2 = param2 - 16; axis_direction = axis_direction + 4 end
- if param2 >= 8 then param2 = param2 - 8; axis_direction = axis_direction + 2 end
- if param2 >= 4 then param2 = param2 - 4; axis_direction = axis_direction + 1 end
- -- if the portal is vertical then node axis direction will be +Y (up) and portal orientation
- -- will set the node's rotation.
- -- if the portal is horizontal then the node axis direction reflects the yaw orientation and
- -- the node's rotation will be whatever's needed to keep the texture horizontal (either 0 or 1)
- if axis_direction == 0 or axis_direction == 5 then
- -- portal is vertical
- return param2 * 90
- else
- if axis_direction == 1 then return 0 end
- if axis_direction == 3 then return 90 end
- if axis_direction == 2 then return 180 end
- if axis_direction == 4 then return 270 end
- end
- end
- -- We want wormhole nodes to only emit mesecon energy orthogonally to the
- -- wormhole surface so that the wormhole will not send power to the frame,
- -- this allows the portal frame to listen for mesecon energy from external switches/wires etc.
- function get_mesecon_emission_rules_from_colorfacedir(param2)
- local axis_direction = 0
- -- Strip off the top 6 bits to leave the 2 rotation bits, unfortunately MT lua has no bitwise '&'
- -- (high 3 bits are palette, followed by 3 direction bits then 2 rotation bits)
- if param2 >= 128 then param2 = param2 - 128 end
- if param2 >= 64 then param2 = param2 - 64 end
- if param2 >= 32 then param2 = param2 - 32 end
- if param2 >= 16 then param2 = param2 - 16; axis_direction = axis_direction + 4 end
- if param2 >= 8 then param2 = param2 - 8; axis_direction = axis_direction + 2 end
- if param2 >= 4 then param2 = param2 - 4; axis_direction = axis_direction + 1 end
- -- if the portal is vertical then node axis_direction will be +Y (up) and node rotation
- -- will reflect the portal's yaw orientation.
- -- If the portal is horizontal then the node axis direction reflects the yaw orientation and
- -- the node's rotation will be whatever's needed to keep the texture horizontal (either 0 or 1)
- local rules
- if axis_direction == 0 or axis_direction == 5 then
- -- portal is vertical
- rules = {{x = 0, y = 0, z = 1}, {x = 0, y = 0, z = -1}}
- if param2 % 2 ~= 0 then
- rules = mesecon.rotate_rules_right(rules)
- end
- else
- -- portal is horizontal, only emit up
- rules = {{x = 0, y = 1, z = 0}}
- end
- return rules
- end
- nether.get_mesecon_emission_rules_from_colorfacedir = get_mesecon_emission_rules_from_colorfacedir -- make the function available to nodes.lua
- -- Combining frame_node_name, p1, and p2 will always be enough to uniquely identify a portal_definition
- -- WITHOUT needing to inspect the world. register_portal() will enforce this.
- -- This function does not require the portal to be in a loaded chunk.
- -- Returns nil if no portal_definition matches the arguments
- local function get_portal_definition(frame_node_name, p1, p2)
- local size = vector.add(vector.subtract(p2, p1), 1)
- local rotated_size = {x = size.z, y = size.y, z = size.x}
- for _, portal_def in pairs(nether.registered_portals) do
- if portal_def.frame_node_name == frame_node_name then
- if vector.equals(size, portal_def.shape.size) or vector.equals(rotated_size, portal_def.shape.size) then
- return portal_def
- end
- end
- end
- return nil
- end
- -- Returns a list of all portal_definitions with a frame made of frame_node_name.
- -- Ideally no two portal types will be built from the same frame material so this call might be enough
- -- to uniquely identify a portal_definition without needing to inspect the world, HOWEVER we shouldn't
- -- cramp anyone's style and prohibit non-nether use of obsidian to make portals, so it returns a list.
- -- If the list contains more than one item then routines like ignite_portal() will have to search twice
- -- for a portal and take twice the CPU.
- local function list_portal_definitions_for_frame_node(frame_node_name)
- local result = {}
- for _, portal_def in pairs(nether.registered_portals) do
- if portal_def.frame_node_name == frame_node_name then table.insert(result, portal_def) end
- end
- return result
- end
- -- Add portal information to mod storage, so new portals may find existing portals near the target location.
- -- Do this whenever a portal is created or changes its ignition state
- local function store_portal_location_info(portal_name, anchorPos, orientation, ignited)
- if not DEBUG_IGNORE_MODSTORAGE then
- local key = minetest.pos_to_string(anchorPos) .. " is " .. portal_name
- if DEBUG then minetest.chat_send_all("Adding/updating portal in mod_storage: " .. key) end
- mod_storage:set_string(
- key,
- minetest.serialize({orientation = orientation, active = ignited})
- )
- end
- end
- -- Remove portal information from mod storage.
- -- Do this if a portal frame is destroyed such that it cannot be ignited anymore.
- local function remove_portal_location_info(portal_name, anchorPos)
- if not DEBUG_IGNORE_MODSTORAGE then
- local key = minetest.pos_to_string(anchorPos) .. " is " .. portal_name
- if DEBUG then minetest.chat_send_all("Removing portal from mod_storage: " .. key) end
- mod_storage:set_string(key, "")
- end
- end
- -- Returns a table of the nearest portals to anchorPos indexed by distance, based on mod_storage
- -- data.
- -- Only portals in the same realm as the anchorPos will be returned, even if y_factor is 0.
- -- WARNING: Portals are not checked, and inactive portals especially may have been damaged without
- -- being removed from the mod_storage data. Check these portals still exist before using them, and
- -- invoke remove_portal_location_info() on any found to no longer exist.
- --
- -- A y_factor of 0 means y does not affect the distance_limit, a y_factor of 1 means y is included,
- -- and a y_factor of 2 would squash the search-sphere by a factor of 2 on the y-axis, etc.
- -- Pass a nil or negative distance_limit to indicate no distance limit
- local function list_closest_portals(portal_definition, anchorPos, distance_limit, y_factor)
- local result = {}
- if not DEBUG_IGNORE_MODSTORAGE then
- local isRealm = portal_definition.is_within_realm(anchorPos)
- if distance_limit == nil then distance_limit = -1 end
- if y_factor == nil then y_factor = 1 end
- for key, value in pairs(mod_storage:to_table().fields) do
- local closingBrace = key:find(")", 6, true)
- if closingBrace ~= nil then
- local found_anchorPos = minetest.string_to_pos(key:sub(0, closingBrace))
- if found_anchorPos ~= nil and portal_definition.is_within_realm(found_anchorPos) == isRealm then
- local found_name = key:sub(closingBrace + 5)
- if found_name == portal_definition.name then
- local x = anchorPos.x - found_anchorPos.x
- local y = anchorPos.y - found_anchorPos.y
- local z = anchorPos.z - found_anchorPos.z
- local distance = math.hypot(y * y_factor, math.hypot(x, z))
- if distance <= distance_limit or distance_limit < 0 then
- local info = minetest.deserialize(value) or {}
- 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
- info.anchorPos = found_anchorPos
- info.distance = distance
- result[distance] = info
- end
- end
- end
- end
- end
- end
- return result
- end
- -- the timerNode is used to keep the metadata as that node already needs to be known any time a portal is stopped or run
- -- see also ambient_sound_stop()
- function ambient_sound_play(portal_definition, soundPos, timerNodeMeta)
- if portal_definition.sounds.ambient ~= nil then
- local soundLength = portal_definition.sounds.ambient.length
- if soundLength == nil then soundLength = 3 end
- local lastPlayed = timerNodeMeta:get_int("ambient_sound_last_played")
- -- Using "os.time() % soundLength == 0" is lightweight but means delayed starts, so trying a stored lastPlayed
- if os.time() >= lastPlayed + soundLength then
- local soundHandle = minetest.sound_play(portal_definition.sounds.ambient, {pos = soundPos, max_hear_distance = 8})
- if timerNodeMeta ~= nil then
- timerNodeMeta:set_int("ambient_sound_handle", soundHandle)
- timerNodeMeta:set_int("ambient_sound_last_played", os.time())
- end
- end
- end
- end
- -- the timerNode is used to keep the metadata as that node already needs to be known any time a portal is stopped or run
- -- see also ambient_sound_play()
- function ambient_sound_stop(timerNodeMeta)
- if timerNodeMeta ~= nil then
- local soundHandle = timerNodeMeta:get_int("ambient_sound_handle")
- minetest.sound_fade(soundHandle, -3, 0)
- -- clear the metadata
- timerNodeMeta:set_string("ambient_sound_handle", "")
- timerNodeMeta:set_string("ambient_sound_last_played", "")
- end
- end
- -- WARNING - this is invoked by on_destruct, so you can't assume there's an accesible node at pos
- -- Returns true if a portal was found to extinguish
- function extinguish_portal(pos, node_name, frame_was_destroyed)
- -- mesecons seems to invoke action_off() 6 times every time you place a block?
- if DEBUG then minetest.chat_send_all("extinguish_portal" .. minetest.pos_to_string(pos) .. " " .. node_name) end
- local meta = minetest.get_meta(pos)
- local p1 = minetest.string_to_pos(meta:get_string("p1"))
- local p2 = minetest.string_to_pos(meta:get_string("p2"))
- local target = minetest.string_to_pos(meta:get_string("target"))
- if p1 == nil or p2 == nil then
- if DEBUG then minetest.chat_send_all(" no active portal found to extinguish") end
- return false
- end
- local portal_definition = get_portal_definition(node_name, p1, p2)
- if portal_definition == nil then
- minetest.log("error", "extinguish_portal() invoked on " .. node_name .. " but no registered portal is constructed from " .. node_name)
- return false -- no portal frames are made from this type of node
- end
- if portal_definition.sounds.extinguish ~= nil then
- minetest.sound_play(portal_definition.sounds.extinguish, {pos = p1})
- end
- -- stop timer and ambient sound
- local timerPos = get_timerPos_from_p1_and_p2(p1, p2)
- minetest.get_node_timer(timerPos):stop()
- ambient_sound_stop(minetest.get_meta(timerPos))
- -- update the ignition state in the portal location info
- local anchorPos, orientation = portal_definition.shape.get_anchorPos_and_orientation_from_p1_and_p2(p1, p2)
- if frame_was_destroyed then
- remove_portal_location_info(portal_definition.name, anchorPos)
- else
- store_portal_location_info(portal_definition.name, anchorPos, orientation, false)
- end
- local frame_node_name = portal_definition.frame_node_name
- local wormhole_node_name = portal_definition.wormhole_node_name
- for x = p1.x, p2.x do
- for y = p1.y, p2.y do
- for z = p1.z, p2.z do
- local clearPos = {x = x, y = y, z = z}
- local nn = minetest.get_node(clearPos).name
- if nn == frame_node_name or nn == wormhole_node_name then
- if nn == wormhole_node_name then
- minetest.remove_node(clearPos)
- if meseconsAvailable then mesecon.receptor_off(clearPos) end
- end
- local m = minetest.get_meta(clearPos)
- m:set_string("p1", "")
- m:set_string("p2", "")
- m:set_string("target", "")
- m:set_string("portal_type", "")
- end
- end
- end
- end
- if target ~= nil then
- if DEBUG then minetest.chat_send_all(" attempting to also extinguish target with wormholePos " .. minetest.pos_to_string(target)) end
- extinguish_portal(target, node_name)
- end
- if portal_definition.on_extinguish ~= nil then
- portal_definition.on_extinguish(portal_definition, anchorPos, orientation)
- end
- return true
- end
- -- Note: will extinguish any portal using the same nodes that are being set
- local function set_portal_metadata(portal_definition, anchorPos, orientation, destination_wormholePos, ignite)
- 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
- -- Portal position is stored in metadata as p1 and p2 to keep maps compatible with earlier versions of this mod.
- -- p1 is the bottom/west/south corner of the portal, and p2 is the opposite corner, together
- -- they define the bounding volume for the portal.
- local p1, p2 = portal_definition.shape:get_p1_and_p2_from_anchorPos(anchorPos, orientation)
- local p1_string, p2_string = minetest.pos_to_string(p1), minetest.pos_to_string(p2)
- local param2 = get_colorfacedir_from_color_and_orientation(portal_definition.wormhole_node_color, orientation, portal_definition.shape.is_horizontal)
- local mesecon_rules
- if ignite and meseconsAvailable then mesecon_rules = get_mesecon_emission_rules_from_colorfacedir(param2) end
- local update_aborted-- using closures to allow the updateFunc to return extra information - by setting this variable
- local updateFunc = function(pos)
- local meta = minetest.get_meta(pos)
- if ignite then
- local node_name = minetest.get_node(pos).name
- if node_name == "air" then
- minetest.set_node(pos, {name = portal_definition.wormhole_node_name, param2 = param2})
- if meseconsAvailable then mesecon.receptor_on(pos, mesecon_rules) end
- end
- local existing_p1 = meta:get_string("p1")
- if existing_p1 ~= "" then
- local existing_p2 = meta:get_string("p2")
- if existing_p1 ~= p1_string or existing_p2 ~= p2_string then
- 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
- -- this node is already part of another portal, so extinguish that, because nodes only
- -- contain a link in the metadata to one portal, and being part of two allows a slew of bugs
- extinguish_portal(pos, node_name, false)
- -- clear the metadata to avoid causing a loop if extinguish_portal() fails on this node (e.g. it only works on frame nodes)
- meta:set_string("p1", nil)
- meta:set_string("p2", nil)
- meta:set_string("target", nil)
- meta:set_string("portal_type", nil)
- update_aborted = true
- return true -- short-circuit the update
- end
- end
- end
- meta:set_string("p1", minetest.pos_to_string(p1))
- meta:set_string("p2", minetest.pos_to_string(p2))
- meta:set_string("target", minetest.pos_to_string(destination_wormholePos))
- if portal_definition.name ~= "nether_portal" then
- -- Legacy portals won't have this extra metadata, so don't rely on it.
- -- It's not strictly necessary for PortalShape_Traditional as we know that p1 is part of
- -- the frame and we can look up the portal type from p1, p2, and frame node name.
- -- Being able to read this from the metadata means other portal shapes needn't have their
- -- frame at the timerPos, it may handle unloaded nodes better, and it saves an extra call
- -- to minetest.getnode().
- meta:set_string("portal_type", portal_definition.name)
- end
- end
- repeat
- update_aborted = false
- portal_definition.shape.apply_func_to_frame_nodes(anchorPos, orientation, updateFunc)
- portal_definition.shape.apply_func_to_wormhole_nodes(anchorPos, orientation, updateFunc)
- until not update_aborted
- local timerPos = get_timerPos_from_p1_and_p2(p1, p2)
- minetest.get_node_timer(timerPos):start(1)
- store_portal_location_info(portal_definition.name, anchorPos, orientation, true)
- end
- local function set_portal_metadata_and_ignite(portal_definition, anchorPos, orientation, destination_wormholePos)
- set_portal_metadata(portal_definition, anchorPos, orientation, destination_wormholePos, true)
- end
- -- this function returns two bools: portal found, portal is lit
- local function is_portal_at_anchorPos(portal_definition, anchorPos, orientation, force_chunk_load)
- local nodes_are_valid -- using closures to allow the check functions to return extra information - by setting this variable
- local portal_is_ignited -- using closures to allow the check functions to return extra information - by setting this variable
- local frame_node_name = portal_definition.frame_node_name
- local check_frame_Func = function(check_pos)
- local foundName = minetest.get_node(check_pos).name
- if foundName ~= frame_node_name then
- if force_chunk_load and foundName == "ignore" then
- -- area isn't loaded, force loading/emerge of check area
- minetest.get_voxel_manip():read_from_map(check_pos, check_pos)
- foundName = minetest.get_node(check_pos).name
- if DEBUG then minetest.chat_send_all("Forced loading of 'ignore' node at " .. minetest.pos_to_string(check_pos) .. ", got " .. foundName) end
- if foundName ~= frame_node_name then
- nodes_are_valid = false
- return true -- short-circuit the search
- end
- else
- nodes_are_valid = false
- return true -- short-circuit the search
- end
- end
- end
- local wormhole_node_name = portal_definition.wormhole_node_name
- local check_wormhole_Func = function(check_pos)
- local node_name = minetest.get_node(check_pos).name
- if node_name ~= wormhole_node_name then
- portal_is_ignited = false;
- if node_name ~= "air" then
- nodes_are_valid = false
- return true -- short-circuit the search
- end
- end
- end
- nodes_are_valid = true
- portal_is_ignited = true
- portal_definition.shape.apply_func_to_frame_nodes(anchorPos, orientation, check_frame_Func) -- check_frame_Func affects nodes_are_valid, portal_is_ignited
- if nodes_are_valid then
- -- a valid frame exists at anchorPos, check the wormhole is either ignited or unobstructed
- portal_definition.shape.apply_func_to_wormhole_nodes(anchorPos, orientation, check_wormhole_Func) -- check_wormhole_Func affects nodes_are_valid, portal_is_ignited
- end
- return nodes_are_valid, portal_is_ignited and nodes_are_valid -- returns two bools: portal was found, portal is lit
- end
- -- Checks pos, and if it's part of a portal or portal frame then three values are returned: anchorPos, orientation, is_ignited
- -- where orientation is 0 or 90 (0 meaning a portal that faces north/south - i.e. obsidian running east/west)
- local function is_within_portal_frame(portal_definition, pos)
- local width_minus_1 = portal_definition.shape.size.x - 1
- local height_minus_1 = portal_definition.shape.size.y - 1
- local depth_minus_1 = portal_definition.shape.size.z - 1
- for d = -depth_minus_1, depth_minus_1 do
- for w = -width_minus_1, width_minus_1 do
- for y = -height_minus_1, height_minus_1 do
- local testAnchorPos_x = {x = pos.x + w, y = pos.y + y, z = pos.z + d}
- local portal_found, portal_lit = is_portal_at_anchorPos(portal_definition, testAnchorPos_x, 0, true)
- if portal_found then
- return testAnchorPos_x, 0, portal_lit
- else
- -- try orthogonal orientation
- local testForAnchorPos_z = {x = pos.x + d, y = pos.y + y, z = pos.z + w}
- portal_found, portal_lit = is_portal_at_anchorPos(portal_definition, testForAnchorPos_z, 90, true)
- if portal_found then return testForAnchorPos_z, 90, portal_lit end
- end
- end
- end
- end
- end
- -- sets param2 values in the schematic to match facedir values, or 0 if the portalframe-nodedef doesn't use facedir
- local function set_schematic_param2(schematic_table, frame_node_name, frame_node_color)
- local paramtype2 = minetest.registered_nodes[frame_node_name].paramtype2
- local isFacedir = paramtype2 == "facedir" or paramtype2 == "colorfacedir"
- if schematic_table.facedirNodes ~= nil then
- for _, node in ipairs(schematic_table.facedirNodes) do
- if isFacedir and node.facedir ~= nil then
- -- frame_node_color can be nil
- local colorBits = (frame_node_color or math.floor((node.param2 or 0) / 32)) * 32
- node.param2 = node.facedir + colorBits
- else
- node.param2 = 0
- end
- end
- end
- end
- local function build_portal(portal_definition, anchorPos, orientation, destination_wormholePos)
- set_schematic_param2(portal_definition.shape.schematic, portal_definition.frame_node_name, portal_definition.frame_node_color)
- minetest.place_schematic(
- portal_definition.shape.get_schematicPos_from_anchorPos(anchorPos, orientation),
- portal_definition.shape.schematic,
- orientation,
- { -- node replacements
- ["default:obsidian"] = portal_definition.frame_node_name,
- },
- true
- )
- -- set the param2 on wormhole nodes to ensure they are the right color
- local wormholeNode = {
- name = portal_definition.wormhole_node_name,
- param2 = get_colorfacedir_from_color_and_orientation(portal_definition.wormhole_node_color, orientation, portal_definition.shape.is_horizontal)
- }
- portal_definition.shape.apply_func_to_wormhole_nodes(
- anchorPos,
- orientation,
- function(pos) minetest.swap_node(pos, wormholeNode) end
- )
- 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
- set_portal_metadata(portal_definition, anchorPos, orientation, destination_wormholePos)
- if portal_definition.on_created ~= nil then
- portal_definition.on_created(portal_definition, anchorPos, orientation)
- end
- end
- -- Sometimes after a portal is placed, concurrent mapgen routines overwrite it.
- -- Make portals immortal for ~20 seconds after creation
- local function remote_portal_checkup(elapsed, portal_definition, anchorPos, orientation, destination_wormholePos)
- if DEBUG then minetest.chat_send_all("portal checkup at " .. elapsed .. " seconds") end
- local wormholePos = portal_definition.shape.get_wormholePos_from_anchorPos(anchorPos, orientation)
- local wormhole_node = minetest.get_node_or_nil(wormholePos)
- local portalFound, portalLit = false, false
- if wormhole_node ~= nil and wormhole_node.name == portal_definition.wormhole_node_name then
- -- a wormhole node was there, but check the whole frame is intact
- portalFound, portalLit = is_portal_at_anchorPos(portal_definition, anchorPos, orientation, false)
- end
- if not portalFound or not portalLit then
- -- ruh roh
- local message = "Newly created portal at " .. minetest.pos_to_string(anchorPos) .. " was overwritten. Attempting to recreate. Issue spotted after " .. elapsed .. " seconds"
- minetest.log("warning", message)
- if DEBUG then minetest.chat_send_all("!!! " .. message) end
- -- A pre-existing portal frame wouldn't have been immediately overwritten, so no need to check for one, just place the portal.
- build_portal(portal_definition, anchorPos, orientation, destination_wormholePos)
- end
- if elapsed < 10 then -- stop checking after ~20 seconds
- local delay = elapsed * 2
- minetest.after(delay, remote_portal_checkup, elapsed + delay, portal_definition, anchorPos, orientation, destination_wormholePos)
- end
- end
- -- Used to find or build the remote twin after a portal is opened.
- -- If a portal is found that is already lit then it will be extinguished first and its destination_wormholePos updated,
- -- this is to enforce that portals only link together in mutual pairs. It would be better for gameplay if I didn't apply
- -- that restriction, but it would require maintaining an accurate list of every portal that links to a portal so they
- -- could be updated if the portal is destroyed. To keep the code simple I'm going to limit portals to only being the
- -- destination of one lit portal at a time.
- -- * suggested_wormholePos indicates where the portal should be built - note this not an anchorPos!
- -- * suggested_orientation is the suggested schematic rotation to use if no useable portal is found at suggested_wormholePos:
- -- 0, 90, 180, 270 (0 meaning a portal that faces north/south - i.e. obsidian running east/west)
- -- * destination_wormholePos is the wormholePos of the destination portal this one will be linked to.
- --
- -- Returns the final (anchorPos, orientation), as they may differ from the anchorPos and orientation that was
- -- specified if an existing portal was already found there.
- local function locate_or_build_portal(portal_definition, suggested_wormholePos, suggested_orientation, destination_wormholePos)
- 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
- local result_anchorPos;
- local result_orientation;
- -- Searching for an existing portal at wormholePos seems better than at anchorPos, though isn't important
- 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
- if found_anchorPos ~= nil then
- -- A portal is already here, we don't have to build one, though we may need to ignite it
- result_anchorPos = found_anchorPos
- result_orientation = found_orientation
- if is_ignited then
- -- We're about to link to this portal, so if it's already linked to a different portal then
- -- extinguish it, to update the state of the about-to-be-orphaned portal.
- local result_target_str = minetest.get_meta(result_anchorPos):get_string("target")
- local result_target = minetest.string_to_pos(result_target_str)
- if result_target ~= nil and vector.equals(result_target, destination_wormholePos) then
- -- It already links back to the portal the player is teleporting from, so don't
- -- extinguish it or the player's portal will also extinguish.
- 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
- else
- 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
- extinguish_portal(found_anchorPos, portal_definition.frame_node_name, false)
- end
- else
- if DEBUG then minetest.chat_send_all(" Build unnecessary: already an unlit portal at " .. minetest.pos_to_string(found_anchorPos) .. ", orientation " .. result_orientation) end
- end
- -- ignite the portal
- set_portal_metadata_and_ignite(portal_definition, result_anchorPos, result_orientation, destination_wormholePos)
- else
- result_orientation = suggested_orientation
- result_anchorPos = portal_definition.shape.get_anchorPos_from_wormholePos(suggested_wormholePos, result_orientation)
- build_portal(portal_definition, result_anchorPos, result_orientation, destination_wormholePos)
- -- make sure portal isn't overwritten by ongoing generation/emerge
- minetest.after(2, remote_portal_checkup, 2, portal_definition, result_anchorPos, result_orientation, destination_wormholePos)
- end
- return result_anchorPos, result_orientation
- end
- -- invoked when a player attempts to turn obsidian nodes into an open portal
- -- ignition_node_name is optional
- local function ignite_portal(ignition_pos, ignition_node_name)
- if ignition_node_name == nil then ignition_node_name = minetest.get_node(ignition_pos).name end
- if DEBUG then minetest.chat_send_all("IGNITE the " .. ignition_node_name .. " at " .. minetest.pos_to_string(ignition_pos)) end
- -- find which sort of portals are made from the node that was clicked on
- local portal_definition_list = list_portal_definitions_for_frame_node(ignition_node_name)
- for _, portal_definition in ipairs(portal_definition_list) do
- local continue = false -- WRT the for loop, since lua has no continue keyword
- -- check it was a portal frame that the player is trying to ignite
- local anchorPos, orientation, is_ignited = is_within_portal_frame(portal_definition, ignition_pos)
- if anchorPos == nil then
- if DEBUG then minetest.chat_send_all("No " .. portal_definition.name .. " portal frame found at " .. minetest.pos_to_string(ignition_pos)) end
- continue = true -- no portal is here, but perhaps there's more than one portal type we need to search for
- elseif is_ignited then
- -- Found a portal, check its metadata and timer is healthy.
- local repair = false
- local meta = minetest.get_meta(ignition_pos)
- if meta ~= nil then
- local p1, p2, target = meta:get_string("p1"), meta:get_string("p2"), meta:get_string("target")
- if p1 == "" or p2 == "" or target == "" then
- -- metadata is missing, the portal frame node must have been removed without calling
- -- on_destruct - perhaps by an ABM, then replaced - presumably by a player.
- -- allowing reigniting will repair the portal
- if DEBUG then minetest.chat_send_all("Broken portal detected, allowing reignition/repair") end
- repair = true
- else
- 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
- -- Check the portal's timer is running, and fix if it's not.
- -- A portal's timer can stop running if the game is played without that portal type being
- -- registered, e.g. enabling one of the example portals then later disabling it, then enabling it again.
- -- (if this is a frequent problem, then change the value of "run_at_every_load" in the lbm)
- local timer = minetest.get_node_timer(get_timerPos_from_p1_and_p2(minetest.string_to_pos(p1), minetest.string_to_pos(p2)))
- if timer ~= nil and timer:get_timeout() == 0 then
- if DEBUG then minetest.chat_send_all("Portal timer was not running: restarting the timer.") end
- timer:start(1)
- end
- end
- end
- if not repair then return false end -- portal is already ignited (or timer has been fixed)
- end
- if continue == false then
- 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
- local destination_anchorPos, destination_orientation
- if portal_definition.is_within_realm(ignition_pos) then
- destination_anchorPos, destination_orientation = portal_definition.find_surface_anchorPos(anchorPos)
- else
- destination_anchorPos, destination_orientation = portal_definition.find_realm_anchorPos(anchorPos)
- end
- if DEBUG and destination_orientation == nil then minetest.chat_send_all("No destination_orientation given") end
- if destination_orientation == nil then destination_orientation = orientation end
- if destination_anchorPos == nil then
- if DEBUG then minetest.chat_send_all("No portal destination available here!") end
- return false
- else
- local destination_wormholePos = portal_definition.shape.get_wormholePos_from_anchorPos(destination_anchorPos, destination_orientation)
- if DEBUG then minetest.chat_send_all("Destination set to " .. minetest.pos_to_string(destination_anchorPos)) end
- -- ignition/BURN_BABY_BURN
- set_portal_metadata_and_ignite(portal_definition, anchorPos, orientation, destination_wormholePos)
- if portal_definition.sounds.ignite ~= nil then
- local local_wormholePos = portal_definition.shape.get_wormholePos_from_anchorPos(anchorPos, orientation)
- minetest.sound_play(portal_definition.sounds.ignite, {pos = local_wormholePos, max_hear_distance = 20})
- end
- if portal_definition.on_ignite ~= nil then
- portal_definition.on_ignite(portal_definition, anchorPos, orientation)
- end
- return true
- end
- end
- end
- end
- -- invoked when a player is standing in a portal
- local function ensure_remote_portal_then_teleport(playerName, portal_definition, local_anchorPos, local_orientation, destination_wormholePos)
- local player = minetest.get_player_by_name(playerName)
- if player == nil then return end -- player quit the game while teleporting
- local playerPos = player:get_pos()
- if playerPos == nil then return end -- player quit the game while teleporting
- -- check player is still standing in a portal
- playerPos.y = playerPos.y + 0.1 -- Fix some glitches at -8000
- if minetest.get_node(playerPos).name ~= portal_definition.wormhole_node_name then
- return -- the player has moved out of the portal
- end
- -- debounce - check player is still standing in the *same* portal that called this function
- local meta = minetest.get_meta(playerPos)
- local local_p1, local_p2 = portal_definition.shape:get_p1_and_p2_from_anchorPos(local_anchorPos, local_orientation)
- local p1_at_playerPos = minetest.string_to_pos(meta:get_string("p1"))
- if p1_at_playerPos == nil or not vector.equals(local_p1, p1_at_playerPos) then
- 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
- return -- the player already teleported, and is now standing in a different portal
- end
- local dest_wormhole_node = minetest.get_node_or_nil(destination_wormholePos)
- if dest_wormhole_node == nil then
- -- area not emerged yet, delay and retry
- 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
- minetest.after(1, ensure_remote_portal_then_teleport, playerName, portal_definition, local_anchorPos, local_orientation, destination_wormholePos)
- else
- local local_wormholePos = portal_definition.shape.get_wormholePos_from_anchorPos(local_anchorPos, local_orientation)
- if dest_wormhole_node.name == portal_definition.wormhole_node_name then
- -- portal exists
- local destination_orientation = get_orientation_from_colorfacedir(dest_wormhole_node.param2)
- local destination_anchorPos = portal_definition.shape.get_anchorPos_from_wormholePos(destination_wormholePos, destination_orientation)
- portal_definition.shape.disable_portal_trap(destination_anchorPos, destination_orientation)
- -- if the portal is already linked to a different portal then extinguish the other portal and
- -- update the target portal to point back at this one.
- local remoteMeta = minetest.get_meta(destination_wormholePos)
- local remoteTarget = minetest.string_to_pos(remoteMeta:get_string("target"))
- if remoteTarget == nil then
- if DEBUG then minetest.chat_send_all("Failed to test whether target portal links back to this one") end
- elseif not vector.equals(remoteTarget, local_wormholePos) then
- if DEBUG then minetest.chat_send_all("Target portal is already linked, extinguishing then relighting to point back at this one") end
- extinguish_portal(remoteTarget, portal_definition.frame_node_name, false)
- set_portal_metadata_and_ignite(
- portal_definition,
- destination_anchorPos,
- destination_orientation,
- local_wormholePos
- )
- end
- 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
- -- play the teleport sound
- if portal_definition.sounds.teleport ~= nil then
- minetest.sound_play(portal_definition.sounds.teleport, {to_player = playerName})
- end
- -- rotate the player if the destination portal is a different orientation
- local rotation_angle = math.rad(destination_orientation - local_orientation)
- local offset = vector.subtract(playerPos, local_wormholePos) -- preserve player's position in the portal
- 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}
- local new_playerPos = vector.add(destination_wormholePos, rotated_offset)
- player:set_pos(new_playerPos)
- player:set_look_horizontal(player:get_look_horizontal() + rotation_angle)
- if portal_definition.on_player_teleported ~= nil then
- portal_definition.on_player_teleported(portal_definition, player, playerPos, new_playerPos)
- end
- else
- -- no wormhole node at destination - destination portal either needs to be built or ignited.
- -- Note: A very rare edge-case that is difficult to set up:
- -- If the destination portal is unlit and its frame shares a node with a lit portal that is linked to this
- -- portal (but has not been travelled through, thus not linking this portal back to it), then igniting
- -- the destination portal will extinguish the portal it's touching, which will extinguish this portal
- -- which will leave a confused player.
- -- I don't think this is worth preventing, but I document it incase someone describes entering a portal
- -- and then the portal turning off.
- 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
- local new_dest_anchorPos, new_dest_orientation = locate_or_build_portal(portal_definition, destination_wormholePos, local_orientation, local_wormholePos)
- local new_dest_wormholePos = portal_definition.shape.get_wormholePos_from_anchorPos(new_dest_anchorPos, new_dest_orientation)
- if not vector.equals(destination_wormholePos, new_dest_wormholePos) then
- -- Update the local portal's target to match where the existing remote portal was found
- if minetest.get_meta(local_anchorPos):get_string("target") == "" then
- -- The local portal has been extinguished!
- -- Abort setting its metadata as that assumes it is active.
- -- This shouldn't happen and may indicate a bug, I trap it incase when the destination
- -- portal was found and extinguished, it somehow linked back to the local portal in a
- -- misaligned fashion that wasn't recognized as being the local portal and caused the
- -- local portal to also be extinguished.
- 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)
- minetest.log("error", message)
- if DEBUG then minetest.chat_send_all("!ERROR! - " .. message) end
- else
- destination_wormholePos = new_dest_wormholePos
- if DEBUG then minetest.chat_send_all(" updating target to where remote portal was found - " .. minetest.pos_to_string(destination_wormholePos)) end
- set_portal_metadata(
- portal_definition,
- local_anchorPos,
- local_orientation,
- destination_wormholePos
- )
- end
- end
- minetest.after(0.1, ensure_remote_portal_then_teleport, playerName, portal_definition, local_anchorPos, local_orientation, destination_wormholePos)
- end
- end
- end
- -- run_wormhole() is invoked once per second per portal, handling teleportation and particle effects.
- -- See get_timerPos_from_p1_and_p2() for an explanation of the timerPos location
- function run_wormhole(timerPos, time_elapsed)
- local portal_definition -- will be used inside run_wormhole_node_func()
- local run_wormhole_node_func = function(pos)
- if math.random(2) == 1 then -- lets run only 3 particlespawners instead of 6 per portal
- minetest.add_particlespawner({
- amount = 16,
- time = 2,
- minpos = {x = pos.x - 0.25, y = pos.y - 0.25, z = pos.z - 0.25},
- maxpos = {x = pos.x + 0.25, y = pos.y + 0.25, z = pos.z + 0.25},
- minvel = {x = -0.8, y = -0.8, z = -0.8},
- maxvel = {x = 0.8, y = 0.8, z = 0.8},
- minacc = {x = 0, y = 0, z = 0},
- maxacc = {x = 0, y = 0, z = 0},
- minexptime = 0.5,
- maxexptime = 1.7,
- minsize = 0.5 * portal_definition.particle_texture_scale,
- maxsize = 1.5 * portal_definition.particle_texture_scale,
- collisiondetection = false,
- texture = portal_definition.particle_texture_colored,
- animation = portal_definition.particle_texture_animation,
- glow = 5
- })
- end
- for _, obj in ipairs(minetest.get_objects_inside_radius(pos, 1)) do
- if obj:is_player() then
- local meta = minetest.get_meta(pos)
- local destination_wormholePos = minetest.string_to_pos(meta:get_string("target"))
- local local_p1 = minetest.string_to_pos(meta:get_string("p1"))
- local local_p2 = minetest.string_to_pos(meta:get_string("p2"))
- if destination_wormholePos ~= nil and local_p1 ~= nil and local_p2 ~= nil then
- -- force emerge of target area
- minetest.get_voxel_manip():read_from_map(destination_wormholePos, destination_wormholePos) -- force load
- if minetest.get_node_or_nil(destination_wormholePos) == nil then
- minetest.emerge_area(vector.subtract(destination_wormholePos, 4), vector.add(destination_wormholePos, 4))
- end
- local local_anchorPos, local_orientation = portal_definition.shape.get_anchorPos_and_orientation_from_p1_and_p2(local_p1, local_p2)
- local playerName = obj:get_player_name()
- minetest.after(
- 3, -- hopefully target area is emerged in 3 seconds
- function()
- ensure_remote_portal_then_teleport(
- playerName,
- portal_definition,
- local_anchorPos,
- local_orientation,
- destination_wormholePos
- )
- end
- )
- end
- end
- end
- end
- local p1, p2, portal_name
- local meta = minetest.get_meta(timerPos)
- if meta ~= nil then
- p1 = minetest.string_to_pos(meta:get_string("p1"))
- p2 = minetest.string_to_pos(meta:get_string("p2"))
- 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
- end
- if p1 ~= nil and p2 ~= nil then
- -- figure out the portal shape so we know where the wormhole nodes will be located
- local frame_node_name
- if portal_name ~= nil and nether.registered_portals[portal_name] ~= nil then
- portal_definition = nether.registered_portals[portal_name]
- else
- frame_node_name = minetest.get_node(timerPos).name -- timerPos should be a frame node if the shape is traditionalPortalShape
- portal_definition = get_portal_definition(frame_node_name, p1, p2)
- end
- if portal_definition == nil then
- 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 .. "\"")
- else
- local anchorPos, orientation = portal_definition.shape.get_anchorPos_and_orientation_from_p1_and_p2(p1, p2)
- portal_definition.shape.apply_func_to_wormhole_nodes(anchorPos, orientation, run_wormhole_node_func)
- if portal_definition.on_run_wormhole ~= nil then
- portal_definition.on_run_wormhole(portal_definition, anchorPos, orientation)
- end
- local wormholePos = portal_definition.shape.get_wormholePos_from_anchorPos(anchorPos, orientation)
- ambient_sound_play(portal_definition, wormholePos, meta)
- end
- end
- end
- local function create_book(item_name, inventory_description, inventory_image, title, author, chapters)
- local display_book = function(itemstack, user, pointed_thing)
- local player_name = user:get_player_name()
- minetest.sound_play("nether_book_open", {to_player = player_name, gain = 0.25})
- local formspec =
- "size[18,12.122]" ..
- "background[0,0;18,11;nether_book_background.png;true]"..
- "image_button_exit[17.3,0;0.8,0.8;nether_book_close.png;;]"..
- "label[3.1,0.5;" .. minetest.formspec_escape(title) .. "]" ..
- "label[3.6,0.9;" .. author .. "]"
- local image_x_adj = -0.4
- local image_width = 1.6
- local image_padding = 0.06
- for i, chapter in ipairs(chapters) do
- local left_margin = 0.9
- local top_margin = 1.7
- local width = 7.9
- local height = 12.0
- local item_number = i
- local items_on_page = math.floor(#chapters / 2)
- if i > items_on_page then
- -- page 2
- left_margin = 10.1
- top_margin = 0.8
- height = 12.9
- item_number = i - items_on_page
- items_on_page = #chapters - items_on_page
- end
- local available_height = (height - top_margin) / items_on_page
- local y = top_margin + (item_number - 1) * available_height
- -- add chapter title
- local title_height = 0
- if chapter.title ~= nil then
- title_height = 0.6
- formspec = formspec .. "label[".. left_margin + 1.5 .. ","
- .. y .. "; ──══♦♦♦◊ " .. minetest.formspec_escape(chapter.title) .. " ◊♦♦♦══──]"
- end
- -- add chapter image
- local x_offset = 0
- if chapter.image ~= nil then
- x_offset = image_width + image_x_adj + image_padding
- local image_height = image_width / chapter.image.width * chapter.image.height
- formspec = formspec .. "image[" .. left_margin + image_x_adj .. "," .. y + title_height .. ";" .. image_width .. ","
- .. image_height .. ";" .. chapter.image.image .. "]"
- end
- -- add chapter text
- formspec = formspec .. "textarea[" .. left_margin + x_offset .. "," .. y + title_height .. ";" .. width - x_offset .. ","
- .. available_height - title_height .. ";;" .. minetest.formspec_escape(chapter.text) .. ";]"
- end
- minetest.show_formspec(player_name, item_name, formspec)
- end
- minetest.register_craftitem(item_name, {
- description = inventory_description,
- inventory_image = inventory_image,
- groups = {book = 1, not_in_creative_inventory=1},
- on_use = display_book,
- _doc_items_hidden = true,
- _doc_items_longdesc =
- 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" ..
- S("Refer: \"Help\" > \"Basics\" > \"Building Portals\""),
- })
- end
- local function add_book_as_treasure()
- book_added_as_treasure = true
- if minetest.get_modpath("loot") then
- loot.register_loot({
- weights = { generic = nether.PORTAL_BOOK_LOOT_WEIGHTING * 1000,
- books = 100 },
- payload = { stack = "nether:book_of_portals" }
- })
- end
- if minetest.get_modpath("dungeon_loot") then
- dungeon_loot.register({name = "nether:book_of_portals", chance = nether.PORTAL_BOOK_LOOT_WEIGHTING})
- end
- -- todo: add to Treasurer mod TRMP https://github.com/poikilos/trmp_minetest_game
- end
- -- Returns true if the Help-modpack was installed and Portal instructions were added to it
- -- Help-modpack details can be found at https://forum.minetest.net/viewtopic.php?t=15912
- local function add_book_to_help_modpack(chapters)
- local result = false
- if minetest.get_modpath("doc") ~= nil and minetest.global_exists("doc") then
- if minetest.get_modpath("doc_basics") ~= nil then
- 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")
- local images = {}
- for i, chapter in ipairs(chapters) do
- if chapter.image ~= nil then
- -- Portal chapters have images (from their portalDef.shape)
- text = text .. "\n\n\n" .. (i - 1) .. ") " .. chapter.title .. "\n\n"
- local aspect_3_to_2_width = chapter.image.width
- local aspect_3_to_2_height = aspect_3_to_2_width / 3 * 2
- if chapter.image.height > aspect_3_to_2_height then
- aspect_3_to_2_height = chapter.image.height
- aspect_3_to_2_width = aspect_3_to_2_height / 2 * 3
- end
- local image_conveted_to_3_2_ratio =
- "[combine:"..aspect_3_to_2_width.."x"..aspect_3_to_2_height..":0,0="..chapter.image.image
- images[#images + 1] = {image=image_conveted_to_3_2_ratio}
- end
- text = text .. chapter.text
- end
- result = doc.add_entry("basics", "portals_api", {
- name = S("Building Portals"),
- data = {
- text = text,
- images = images,
- aspect_ratio=.5
- }
- })
- end
- end
- return result
- end
- -- Updates nether:book_of_portals
- -- A book the player can read to lean how to build the different portals
- local function create_book_of_portals()
- local chapters = {}
- local intro_text
- -- tell the player how many portal types there are
- if nether.registered_portals_count == 1 then
- 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.")
- else
- 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)
- end
- -- tell the player how to ignite portals
- local ignition_item_description = "<error - ignition item not set>"
- if ignition_item_name ~= nil and minetest.registered_items[ignition_item_name] ~= nil then
- ignition_item_description = minetest.registered_items[ignition_item_name].description
- end
- intro_text = intro_text ..
- 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))
- chapters[#chapters + 1] = {text = intro_text}
- -- Describe how to create each type of portal, or perhaps just give clues or flavor text,
- -- but ensure the Nether is always listed first on the first page so other definitions can
- -- refer to it (pairs() returns order based on a random hash).
- local portalDefs_in_order = {}
- if nether.registered_portals["nether_portal"] then
- portalDefs_in_order[#portalDefs_in_order + 1] = nether.registered_portals["nether_portal"]
- end
- for portalName, portalDef in pairs(nether.registered_portals) do
- if portalName ~= "nether_portal" then
- portalDefs_in_order[#portalDefs_in_order + 1] = portalDef
- end
- end
- for _, portalDef in ipairs(portalDefs_in_order) do
- chapters[#chapters + 1] = {
- text = portalDef.book_of_portals_pagetext,
- image = portalDef.shape.diagram_image,
- title = portalDef.title
- }
- end
- create_book(
- ":nether:book_of_portals",
- S("Book of Portals"),
- "nether_book_of_portals.png",
- S("A definitive guide to Rifts and Portals"),
- "Riccard F. Burton", -- perhaps a Richard F. Burton of an alternate universe
- chapters
- )
- local using_helpModpack = add_book_to_help_modpack(chapters)
- if not using_helpModpack and not book_added_as_treasure and nether.PORTAL_BOOK_LOOT_WEIGHTING > 0 then
- -- Only place the Book of Portals in chests if there are non-Nether (i.e. novel) portals
- -- which players need a way to find out about.
- if nether.registered_portals_count > 1 or (nether.registered_portals_count == 1 and nether.registered_portals["nether_portal"] == nil) then
- add_book_as_treasure()
- end
- end
- end
- function register_frame_node(frame_node_name)
- -- copy the existing node definition
- local node_def = minetest.registered_nodes[frame_node_name]
- local extended_node_def = {}
- for key, value in pairs(node_def) do extended_node_def[key] = value end
- extended_node_def.replaced_by_portalapi = {} -- allows chaining or restoration of original functions, if necessary
- -- add portal portal functionality
- extended_node_def.replaced_by_portalapi.mesecons = extended_node_def.mesecons
- extended_node_def.mesecons = {effector = {
- action_on = function (pos, node)
- if DEBUG then minetest.chat_send_all("portal frame material: mesecons action ON") end
- ignite_portal(pos, node.name)
- end,
- action_off = function (pos, node)
- if DEBUG then minetest.chat_send_all("portal frame material: mesecons action OFF") end
- extinguish_portal(pos, node.name, false)
- end
- }}
- extended_node_def.replaced_by_portalapi.on_destruct = extended_node_def.on_destruct
- extended_node_def.on_destruct = function(pos)
- if DEBUG then minetest.chat_send_all("portal frame material: destruct") end
- extinguish_portal(pos, frame_node_name, true)
- end
- extended_node_def.replaced_by_portalapi.on_blast = extended_node_def.on_blast
- extended_node_def.on_blast = function(pos, intensity)
- if DEBUG then minetest.chat_send_all("portal frame material: blast") end
- extinguish_portal(pos, frame_node_name, extended_node_def.replaced_by_portalapi.on_blast == nil)
- if extended_node_def.replaced_by_portalapi.on_blast ~= nil then
- extended_node_def.replaced_by_portalapi.on_blast(pos, intensity)
- else
- minetest.remove_node(pos)
- end
- end
- extended_node_def.replaced_by_portalapi.on_timer = extended_node_def.on_timer
- extended_node_def.on_timer = function(pos, elapsed)
- run_wormhole(pos, elapsed)
- return true
- end
- -- replace the node with the new extended definition
- minetest.register_node(":" .. frame_node_name, extended_node_def)
- end
- function unregister_frame_node(frame_node_name)
- -- copy the existing node definition
- local node = minetest.registered_nodes[frame_node_name]
- local restored_node_def = {}
- for key, value in pairs(node) do restored_node_def[key] = value end
- -- remove portal portal functionality
- restored_node_def.mesecons = nil
- restored_node_def.on_destruct = nil
- restored_node_def.on_timer = nil
- restored_node_def.replaced_by_portalapi = nil
- if node.replaced_by_portalapi ~= nil then
- for key, value in pairs(node.replaced_by_portalapi) do restored_node_def[key] = value end
- end
- -- replace the node with the restored definition
- minetest.register_node(":" .. frame_node_name, restored_node_def)
- end
- -- check for mistakes people might make in custom shape definitions
- function test_shapedef_is_valid(shape_defintion)
- assert(shape_defintion ~= nil, "shape definition cannot be nil")
- assert(shape_defintion.name ~= nil, "shape definition must have a name")
- local result = true
- local origin = vector.new()
- local p1, p2 = shape_defintion:get_p1_and_p2_from_anchorPos(origin, 0)
- 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")
- 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")
- 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")
- -- todo
- return result
- end
- -- check for mistakes people might make in portal definitions
- function test_portaldef_is_valid(portal_definition)
- local result = test_shapedef_is_valid(portal_definition.shape)
- 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)")
- assert(portal_definition.is_within_realm ~= nil, "portaldef.is_within_realm() must be implemented")
- assert(portal_definition.find_realm_anchorPos ~= nil, "portaldef.find_realm_anchorPos() must be implemented")
- if portal_definition.frame_node_color ~= nil then
- 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)")
- end
- -- todo
- return result
- end
- -- convert portals made with old ABM version of nether mod to use the timer instead
- minetest.register_lbm({
- label = "Start portal timer",
- name = "nether:start_portal_timer",
- nodenames = {"nether:portal"},
- run_at_every_load = false,
- action = function(pos, node)
- local p1, p2
- local meta = minetest.get_meta(pos)
- if meta ~= nil then
- p1 = minetest.string_to_pos(meta:get_string("p1"))
- p2 = minetest.string_to_pos(meta:get_string("p2"))
- end
- if p1 ~= nil and p2 ~= nil then
- local timerPos = get_timerPos_from_p1_and_p2(p1, p2)
- local timer = minetest.get_node_timer(timerPos)
- if timer ~= nil then
- timer:start(1)
- if DEBUG then minetest.chat_send_all("LBM started portal timer " .. minetest.pos_to_string(timerPos)) end
- elseif DEBUG then
- minetest.chat_send_all("get_node_timer" .. minetest.pos_to_string(timerPos) .. " returned null")
- end
- end
- end
- })
- -- Portal API functions --
- -- ==================== --
- -- the fallback defaults for wormhole nodedefs
- local wormhole_nodedef_default = {
- description = S("Portal wormhole"),
- tiles = {
- "nether_transparent.png",
- "nether_transparent.png",
- "nether_transparent.png",
- "nether_transparent.png",
- {
- name = "nether_portal.png",
- animation = {
- type = "vertical_frames",
- aspect_w = 16,
- aspect_h = 16,
- length = 0.9,
- },
- },
- {
- name = "nether_portal.png",
- animation = {
- type = "vertical_frames",
- aspect_w = 16,
- aspect_h = 16,
- length = 0.9,
- },
- },
- },
- drawtype = "nodebox",
- paramtype = "light",
- paramtype2 = "colorfacedir",
- palette = "nether_portals_palette.png",
- post_effect_color = {
- -- post_effect_color can't be changed dynamically in Minetest like the portal colour is.
- -- If you need a different post_effect_color then use register_wormhole_node() to create
- -- another wormhole node with the right post_effect_color and set it as the wormhole_node_name
- -- in your portaldef.
- -- Hopefully this colour is close enough to magenta to work with the traditional magenta
- -- portals, close enough to red to work for a red portal, and also close enough to red to
- -- work with blue & cyan portals - since blue portals are sometimes portrayed as being red
- -- from the opposite side / from the inside.
- a = 160, r = 128, g = 0, b = 80
- },
- sunlight_propagates = true,
- use_texture_alpha = true,
- walkable = false,
- diggable = false,
- pointable = false,
- buildable_to = false,
- is_ground_content = false,
- drop = "",
- light_source = 5,
- alpha = 192,
- node_box = {
- type = "fixed",
- fixed = {
- {-0.5, -0.5, -0.1, 0.5, 0.5, 0.1},
- },
- },
- groups = {not_in_creative_inventory = 1},
- mesecons = {receptor = {
- state = "on",
- rules = function(node)
- return nether.get_mesecon_emission_rules_from_colorfacedir(node.param2)
- end
- }}
- }
- -- Call only at load time
- function nether.register_wormhole_node(name, nodedef)
- assert(name ~= nil, "Unable to register wormhole node: Name is nil")
- assert(nodedef ~= nil, "Unable to register wormhole node ''" .. name .. "'': nodedef is nil")
- for key, value in pairs(wormhole_nodedef_default) do
- if nodedef[key] == nil then nodedef[key] = value end
- end
- minetest.register_node(name, nodedef)
- end
- -- The fallback defaults for registered portaldef tables
- local portaldef_default = {
- title = S("Untitled portal"),
- book_of_portals_pagetext = S("We know almost nothing about this portal"),
- shape = nether.PortalShape_Traditional,
- wormhole_node_name = "nether:portal",
- wormhole_node_color = 0,
- frame_node_name = "default:obsidian",
- particle_texture = "nether_particle.png",
- particle_texture_animation = nil,
- particle_texture_scale = 1,
- sounds = {
- ambient = {name = "nether_portal_ambient", gain = 0.6, length = 3},
- ignite = {name = "nether_portal_ignite", gain = 0.7},
- extinguish = {name = "nether_portal_extinguish", gain = 0.6},
- teleport = {name = "nether_portal_teleport", gain = 0.3}
- }
- }
- function nether.register_portal(name, portaldef)
- assert(name ~= nil, "Unable to register portal: Name is nil")
- assert(portaldef ~= nil, "Unable to register portal ''" .. name .. "'': portaldef is nil")
- if nether.registered_portals[name] ~= nil then
- minetest.log("error", "Unable to register portal: '" .. name .. "' is already in use")
- return false;
- end
- portaldef.name = name
- portaldef.mod_name = minetest.get_current_modname()
- -- use portaldef_default for any values missing from portaldef or portaldef.sounds
- if portaldef.sounds ~= nil then setmetatable(portaldef.sounds, {__index = portaldef_default.sounds}) end
- setmetatable(portaldef, {__index = portaldef_default})
- if portaldef.particle_color == nil then
- -- default the particle colours to be the same as the wormhole colour
- assert(portaldef.wormhole_node_color >= 0 and portaldef.wormhole_node_color < 8, "portaldef.wormhole_node_color must be between 0 and 7 (inclusive)")
- portaldef.particle_color = nether.portals_palette[portaldef.wormhole_node_color].asString
- end
- if portaldef.particle_texture_colored == nil then
- -- Combine the particle texture with the particle color unless a particle_texture_colored was specified.
- if type(portaldef.particle_texture) == "table" and portaldef.particle_texture.animation ~= nil then
- portaldef.particle_texture_colored = portaldef.particle_texture.name .. "^[colorize:" .. portaldef.particle_color .. ":alpha"
- portaldef.particle_texture_animation = portaldef.particle_texture.animation
- portaldef.particle_texture_scale = portaldef.particle_texture.scale or 1
- else
- portaldef.particle_texture_colored = portaldef.particle_texture .. "^[colorize:" .. portaldef.particle_color .. ":alpha"
- end
- end
- if portaldef.find_surface_anchorPos == nil then -- default to using find_surface_target_y()
- portaldef.find_surface_anchorPos = function(pos)
- local destination_pos = {x = pos.x, y = 0, z = pos.z}
- local existing_portal_location, existing_portal_orientation =
- 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)
- if existing_portal_location ~= nil then
- return existing_portal_location, existing_portal_orientation
- else
- destination_pos.y = nether.find_surface_target_y(destination_pos.x, destination_pos.z, name)
- return destination_pos
- end
- end
- end
- if test_portaldef_is_valid(portaldef) then
- -- check whether the portal definition clashes with anyone else's portal
- local p1, p2 = portaldef.shape:get_p1_and_p2_from_anchorPos(vector.new(), 0)
- local existing_portaldef = get_portal_definition(portaldef.frame_node_name, p1, p2)
- if existing_portaldef ~= nil then
- minetest.log("error",
- portaldef.mod_name .." tried to register a portal '" .. portaldef.name .. "' made of " .. portaldef.frame_node_name ..
- ", but it is the same material and shape as the portal '" .. existing_portaldef.name .. "' already registered by " .. existing_portaldef.mod_name ..
- ". Edit the values one of those mods uses in its call to nether.register_portal() if you wish to resolve this clash.")
- else
- -- the new portaldef is good
- nether.registered_portals[portaldef.name] = portaldef
- -- Update registered_portals_count
- local portalCount = 0
- for _ in pairs(nether.registered_portals) do portalCount = portalCount + 1 end
- nether.registered_portals_count = portalCount
- create_book_of_portals()
- if not nether.is_frame_node[portaldef.frame_node_name] then
- -- add portal functions to the nodedef being used for the portal frame
- register_frame_node(portaldef.frame_node_name)
- nether.is_frame_node[portaldef.frame_node_name] = true
- end
- return true
- end
- end
- return false
- end
- function nether.unregister_portal(name)
- assert(name ~= nil, "Cannot unregister portal: Name is nil")
- local portaldef = nether.registered_portals[name]
- local result = portaldef ~= nil
- if portaldef ~= nil then
- nether.registered_portals[name] = nil
- local portals_still_using_frame_node = list_portal_definitions_for_frame_node(portaldef.frame_node_name)
- if next(portals_still_using_frame_node) == nil then
- -- no portals are using this frame node any more
- unregister_frame_node(portaldef.frame_node_name)
- nether.is_frame_node[portaldef.frame_node_name] = nil
- end
- end
- return result
- end
- function nether.register_portal_ignition_item(item_name, ignition_failure_sound)
- minetest.override_item(item_name, {
- on_place = function(stack, _, pt)
- local done = false
- if pt.under and nether.is_frame_node[minetest.get_node(pt.under).name] then
- done = ignite_portal(pt.under)
- if done and not minetest.settings:get_bool("creative_mode") then
- stack:take_item()
- end
- end
- if not done and ignition_failure_sound ~= nil then
- minetest.sound_play(ignition_failure_sound, {pos = pt.under, max_hear_distance = 10})
- end
- return stack
- end,
- })
- ignition_item_name = item_name
- end
- -- use this when determining where to spawn a portal, to avoid overwriting player builds
- -- It checks the area for any nodes that aren't ground or trees.
- -- (Water also fails this test, unless it is unemerged)
- function nether.volume_is_natural(minp, maxp)
- local c_air = minetest.get_content_id("air")
- local c_ignore = minetest.get_content_id("ignore")
- local vm = minetest.get_voxel_manip()
- local pos1 = {x = minp.x, y = minp.y, z = minp.z}
- local pos2 = {x = maxp.x, y = maxp.y, z = maxp.z}
- local emin, emax = vm:read_from_map(pos1, pos2)
- local area = VoxelArea:new({MinEdge = emin, MaxEdge = emax})
- local data = vm:get_data()
- for z = pos1.z, pos2.z do
- for y = pos1.y, pos2.y do
- local vi = area:index(pos1.x, y, z)
- for x = pos1.x, pos2.x do
- local id = data[vi] -- Existing node
- if DEBUG and id == nil then minetest.chat_send_all("nil block at index " .. vi) end
- if id ~= c_air and id ~= c_ignore and id ~= nil then -- checked for common natural or not emerged
- local name = minetest.get_name_from_content_id(id)
- local nodedef = minetest.registered_nodes[name]
- if not nodedef.is_ground_content then
- -- trees are natural but not "ground content"
- local node_groups = nodedef.groups
- if node_groups == nil or (node_groups.tree == nil and node_groups.leaves == nil and node_groups.leafdecay == nil) then
- if DEBUG then minetest.chat_send_all("volume_is_natural() found unnatural node " .. name) end
- return false
- end
- end
- end
- vi = vi + 1
- end
- end
- end
- if DEBUG then minetest.chat_send_all("Volume is natural") end
- return true
- end
- -- Can be used when implementing custom find_surface_anchorPos() functions
- -- portal_name is optional, providing it allows existing portals on the surface to be reused.
- function nether.find_surface_target_y(target_x, target_z, portal_name)
- assert(target_x ~= nil and target_z ~= nil, "Arguments `target_x` and `target_z` cannot be nil when calling find_surface_target_y()")
- -- default to starting the search at -16 (probably underground) if we don't know the
- -- surface, like paramat's original code from before get_spawn_level() was available:
- -- https://github.com/minetest-mods/nether/issues/5#issuecomment-506983676
- local start_y = -16
- -- try to spawn on surface first
- if minetest.get_spawn_level ~= nil then -- older versions of Minetest don't have this
- local surface_level = minetest.get_spawn_level(target_x, target_z)
- if surface_level ~= nil then -- test this since get_spawn_level() can return nil over water or steep/high terrain
- -- get_spawn_level() tends to err on the side of caution and spawns the player a
- -- block higher than the ground level. The implementation is mapgen specific
- -- and -2 seems to be the right correction for v6, v5, carpathian, valleys, and flat,
- -- but v7 only needs -1.
- -- Perhaps this was not always the case, and -2 may be too much in older versions
- -- of minetest, but half-buried portals are perferable to floating ones, and they
- -- will clear a suitable hole around themselves.
- if minetest.get_mapgen_setting("mg_name") == "v7" then
- surface_level = surface_level - 1
- else
- surface_level = surface_level - 2
- end
- start_y = surface_level
- end
- end
- for y = start_y, start_y - 256, -16 do
- -- Check volume for non-natural nodes
- local minp = {x = target_x - 1, y = y - 1, z = target_z - 2}
- local maxp = {x = target_x + 2, y = y + 3, z = target_z + 2}
- if nether.volume_is_natural(minp, maxp) then
- return y
- elseif portal_name ~= nil and nether.registered_portals[portal_name] ~= nil then
- -- players have built here - don't grief.
- -- but reigniting existing portals in portal rooms is fine - desirable even.
- local anchorPos, orientation, is_ignited = is_within_portal_frame(nether.registered_portals[portal_name], {x = target_x, y = y, z = target_z})
- if anchorPos ~= nil then
- 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
- return y
- end
- end
- end
- return start_y - 256 -- Fallback
- end
- -- Returns the anchorPos, orientation of the nearest portal, or nil.
- -- A y_factor of 0 means y does not affect the distance_limit, a y_factor of 1 means y is included,
- -- and a y_factor of 2 would squash the search-sphere by a factor of 2 on the y-axis, etc.
- -- Pass a negative distance_limit to indicate no distance limit
- function nether.find_nearest_working_portal(portal_name, anchorPos, distance_limit, y_factor)
- local portal_definition = nether.registered_portals[portal_name]
- assert(portal_definition ~= nil, "find_nearest_working_portal() called with portal_name '" .. portal_name .. "', but no portal is registered with that name.")
- assert(anchorPos ~= nil, "Argument `anchorPos` cannot be nil when calling find_nearest_working_portal()")
- local contenders = list_closest_portals(portal_definition, anchorPos, distance_limit, y_factor)
- -- sort by distance
- local dist_list = {}
- for dist, _ in pairs(contenders) do table.insert(dist_list, dist) end
- table.sort(dist_list)
- for _, dist in ipairs(dist_list) do
- local portal_info = contenders[dist]
- 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
- -- the mod_storage list of portals is unreliable - e.g. it won't know if inactive portals have been
- -- destroyed, so check the portal is still there
- local portalFound, portalIsActive = is_portal_at_anchorPos(portal_definition, portal_info.anchorPos, portal_info.orientation, true)
- if portalFound then
- return portal_info.anchorPos, portal_info.orientation
- else
- 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
- -- The portal at that location must have been destroyed
- remove_portal_location_info(portal_name, portal_info.anchorPos)
- end
- end
- return nil
- end
|