init.lua 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640
  1. local mod_name = minetest.get_current_modname()
  2. -- Node replacements that emit light
  3. -- Sets of lighting_node={ node=original_node, level=light_level }
  4. local lighting_nodes = {}
  5. -- The nodes that can be replaced with lighting nodes
  6. -- Sets of original_node={ [1]=lighting_node_1, [2]=lighting_node_2, ... }
  7. local lightable_nodes = {}
  8. -- Prefixes used for each node so we can avoid overlap
  9. -- Pairs of prefix=original_node
  10. local lighting_prefixes = {}
  11. -- node_name=true pairs of lightable nodes that are liquids and can flood some light sources
  12. local lightable_liquids = {}
  13. -- How often will the positions of lights be recalculated
  14. local update_interval = 0.2
  15. -- How long until a previously lit node should be updated - reduces flicker
  16. local removal_delay = update_interval * 0.5
  17. -- How often will a node attempt to check itself for deletion
  18. local cleanup_interval = update_interval * 3
  19. -- How far in the future will the position be projected based on the velocity
  20. local velocity_projection = update_interval * 1
  21. -- How many light levels should an item held in the hand be reduced by, compared to the placed node
  22. -- does not apply to manually registered light levels
  23. local level_delta = 2
  24. -- item=light_level pairs of registered wielded lights
  25. local shiny_items = {}
  26. -- List of custom callbacks for each update step
  27. local update_callbacks = {}
  28. local update_player_callbacks = {}
  29. -- position={id=light_level} sets of known about light sources and their levels by position
  30. local active_lights = {}
  31. --[[ Sets of entities being tracked, in the form:
  32. entity_id = {
  33. obj = entity,
  34. items = {
  35. category_id..entity_id = {
  36. level = light_level,
  37. item? = item_name
  38. }
  39. },
  40. update = true | false,
  41. pos? = position_vector,
  42. offset? = offset_vector,
  43. }
  44. ]]
  45. local tracked_entities = {}
  46. -- position=true pairs of positions that need to be recaculated this update step
  47. local light_recalcs = {}
  48. --[[
  49. Using 2-digit hex codes for categories
  50. Starts at 00, ends at FF
  51. This makes it easier extract `uid` from `cat_id..uid` by slicing off 2 characters
  52. The category ID must be of a fixed length (2 characters)
  53. ]]
  54. local cat_id = 0
  55. local cat_codes = {}
  56. local function get_light_category_id(cat)
  57. -- If the category id does not already exist generate a new one
  58. if not cat_codes[cat] then
  59. if cat_id >= 256 then
  60. error("Wielded item category limit exceeded, maximum 256 wield categories")
  61. end
  62. local code = string.format("%02x", cat_id)
  63. cat_id = cat_id+1
  64. cat_codes[cat] = code
  65. end
  66. -- If the category id does exist, return it
  67. return cat_codes[cat]
  68. end
  69. -- Log an error coming from this mod
  70. local function error_log(message, ...)
  71. minetest.log("error", "[Wielded Light] " .. (message:format(...)))
  72. end
  73. -- Is a node lightable and a liquid capable of flooding some light sources
  74. local function is_lightable_liquid(pos)
  75. local node = minetest.get_node_or_nil(pos)
  76. if not node then return end
  77. return lightable_liquids[node.name]
  78. end
  79. -- Check if an entity instance still exists in the world
  80. local function is_entity_valid(entity)
  81. return entity and (entity.obj:is_player() or (entity.obj:get_luaentity() and entity.obj:get_luaentity().name) or false)
  82. end
  83. -- Check whether a node was registered by the wield_light mod
  84. local function is_wieldlight_node(pos_vec)
  85. local name = string.sub(minetest.get_node(pos_vec).name, 1, #mod_name)
  86. return name == mod_name
  87. end
  88. -- Get the projected position of an entity based on its velocity, rounded to the nearest block
  89. local function entity_pos(obj, offset)
  90. local velocity
  91. if (minetest.features.direct_velocity_on_players or not obj:is_player()) and obj.get_velocity then
  92. velocity = obj:get_velocity()
  93. else
  94. velocity = obj:get_player_velocity()
  95. end
  96. return wielded_light.get_light_position(
  97. vector.round(
  98. vector.add(
  99. vector.add(
  100. offset or { x=0, y=0, z=0 },
  101. obj:get_pos()
  102. ),
  103. vector.multiply(
  104. velocity or { x=0, y=0, z=0 },
  105. velocity_projection
  106. )
  107. )
  108. )
  109. )
  110. end
  111. -- Add light to active light list and mark position for update
  112. local function add_light(pos, id, light_level)
  113. if not active_lights[pos] then
  114. active_lights[pos] = {}
  115. end
  116. if active_lights[pos][id] ~= light_level then
  117. -- minetest.log("error", "add "..id.." "..pos.." "..tostring(light_level))
  118. active_lights[pos][id] = light_level
  119. light_recalcs[pos] = true
  120. end
  121. end
  122. -- Remove light from active light list and mark position for update
  123. local function remove_light(pos, id)
  124. if not active_lights[pos] then return end
  125. -- minetest.log("error", "rem "..id.." "..pos)
  126. active_lights[pos][id] = nil
  127. minetest.after(removal_delay, function ()
  128. light_recalcs[pos] = true
  129. end)
  130. end
  131. -- Track an entity's position and update its light, will be called on every update step
  132. local function update_entity(entity)
  133. local pos = entity_pos(entity.obj, entity.offset)
  134. local pos_str = pos and minetest.pos_to_string(pos)
  135. -- If the position has changed, remove the old light and mark the entity for update
  136. if entity.pos and pos_str ~= entity.pos then
  137. entity.update = true
  138. for id,_ in pairs(entity.items) do
  139. remove_light(entity.pos, id)
  140. end
  141. end
  142. -- Update the recorded position
  143. entity.pos = pos_str
  144. -- If the position is still loaded, pump the timer up so it doesn't get removed
  145. if pos then
  146. -- If the entity is marked for an update, add the light in the position if it emits light
  147. if entity.update then
  148. for id, item in pairs(entity.items) do
  149. if item.level > 0 and not (item.floodable and is_lightable_liquid(pos)) then
  150. add_light(pos_str, id, item.level)
  151. else
  152. remove_light(pos_str, id)
  153. end
  154. end
  155. end
  156. end
  157. if active_lights[pos_str] then
  158. if is_wieldlight_node(pos) then
  159. minetest.get_node_timer(pos):start(cleanup_interval)
  160. end
  161. end
  162. entity.update = false
  163. end
  164. -- Save the original nodes timer if it has one
  165. local function save_timer(pos_vec)
  166. local timer = minetest.get_node_timer(pos_vec)
  167. if timer:is_started() then
  168. local meta = minetest.get_meta(pos_vec)
  169. meta:set_float("saved_timer_timeout", timer:get_timeout())
  170. meta:set_float("saved_timer_elapsed", timer:get_elapsed())
  171. end
  172. end
  173. -- Restore the original nodes timer if it had one
  174. local function restore_timer(pos_vec)
  175. local meta = minetest.get_meta(pos_vec)
  176. local timeout = meta:get_float("saved_timer_timeout")
  177. if timeout > 0 then
  178. local elapsed = meta:get_float("saved_timer_elapsed")
  179. local timer = minetest.get_node_timer(pos_vec)
  180. timer:set(timeout, elapsed)
  181. meta:set_string("saved_timer_timeout","")
  182. meta:set_string("saved_timer_elapsed","")
  183. end
  184. end
  185. -- Replace a lighting node with its original counterpart
  186. local function reset_lighting_node(pos)
  187. local existing_node = minetest.get_node(pos)
  188. local lighting_node = wielded_light.get_lighting_node(existing_node.name)
  189. if not lighting_node then
  190. return
  191. end
  192. minetest.swap_node(pos, { name = lighting_node.node,param2 = existing_node.param2 })
  193. restore_timer(pos)
  194. end
  195. -- Will be run once the node timer expires
  196. local function cleanup_timer_callback(pos, elapsed)
  197. local pos_str = minetest.pos_to_string(pos)
  198. local lights = active_lights[pos_str]
  199. -- If no active lights for this position, remove itself
  200. if not lights then
  201. reset_lighting_node(pos)
  202. else
  203. -- Clean up any tracked entities for this position that no longer exist
  204. for id,_ in pairs(lights) do
  205. local uid = string.sub(id,3)
  206. local entity = tracked_entities[uid]
  207. if not is_entity_valid(entity) then
  208. remove_light(pos_str, id)
  209. end
  210. end
  211. minetest.get_node_timer(pos):start(cleanup_interval)
  212. end
  213. end
  214. -- Recalculate the total light level for a given position and update the light level there
  215. local function recalc_light(pos)
  216. -- If not in active lights list we can't do anything
  217. if not active_lights[pos] then return end
  218. -- Calculate the light level of the node
  219. local any_light = false
  220. local max_light = 0
  221. for id, light_level in pairs(active_lights[pos]) do
  222. any_light = true
  223. if light_level > max_light then
  224. max_light = light_level
  225. end
  226. end
  227. -- Convert the position back to a vector
  228. local pos_vec = minetest.string_to_pos(pos)
  229. -- If no items in this position, delete it from the list and remove any light node
  230. if not any_light then
  231. active_lights[pos] = nil
  232. reset_lighting_node(pos_vec)
  233. return
  234. end
  235. -- If no light in this position remove any light node
  236. if max_light == 0 then
  237. reset_lighting_node(pos_vec)
  238. return
  239. end
  240. -- Limit the light level
  241. max_light = math.min(max_light, minetest.LIGHT_MAX)
  242. -- Get the current light level in this position
  243. local existing_node = minetest.get_node(pos_vec)
  244. local name = existing_node.name
  245. local old_value = wielded_light.level_of_lighting_node(name) or 0
  246. -- If the light level has changed, set the coresponding light node and initiate the cleanup timer
  247. if old_value ~= max_light then
  248. local node_name
  249. if lightable_nodes[name] then
  250. node_name = name
  251. elseif lighting_nodes[name] then
  252. node_name = lighting_nodes[name].node
  253. end
  254. if node_name then
  255. if not is_wieldlight_node(pos_vec) then
  256. save_timer(pos_vec)
  257. end
  258. minetest.swap_node(pos_vec, {
  259. name = lightable_nodes[node_name][max_light],
  260. param2 = existing_node.param2
  261. })
  262. minetest.get_node_timer(pos_vec):start(cleanup_interval)
  263. else
  264. active_lights[pos] = nil
  265. end
  266. end
  267. end
  268. local timer = 0
  269. -- Will be run on every global step
  270. local function global_timer_callback(dtime)
  271. -- Only run once per update interval, global step will be called much more often than that
  272. timer = timer + dtime;
  273. if timer < update_interval then
  274. return
  275. end
  276. timer = 0
  277. -- Run all custom player callbacks for each player
  278. local connected_players = minetest.get_connected_players()
  279. for _,callback in pairs(update_player_callbacks) do
  280. for _, player in pairs(connected_players) do
  281. callback(player)
  282. end
  283. end
  284. -- Run all custom callbacks
  285. for _,callback in pairs(update_callbacks) do
  286. callback()
  287. end
  288. -- Look at each tracked entity and update its position
  289. for uid, entity in pairs(tracked_entities) do
  290. if is_entity_valid(entity) then
  291. update_entity(entity)
  292. else
  293. -- If the entity no longer exists, stop tracking it
  294. tracked_entities[uid] = nil
  295. end
  296. end
  297. -- Recalculate light levels
  298. for pos,_ in pairs(light_recalcs) do
  299. recalc_light(pos)
  300. end
  301. light_recalcs = {}
  302. end
  303. --- Shining API ---
  304. wielded_light = {}
  305. -- Registers a callback to be called every time the update interval is passed
  306. function wielded_light.register_lightstep(callback)
  307. table.insert(update_callbacks, callback)
  308. end
  309. -- Registers a callback to be called for each player every time the update interval is passed
  310. function wielded_light.register_player_lightstep(callback)
  311. table.insert(update_player_callbacks, callback)
  312. end
  313. -- Returns the node name for a given light level
  314. function wielded_light.lighting_node_of_level(light_level, prefix)
  315. return mod_name..":"..(prefix or "")..light_level
  316. end
  317. -- Gets the light level for a given node name, inverse of lighting_node_of_level
  318. function wielded_light.level_of_lighting_node(node_name)
  319. local lighting_node = wielded_light.get_lighting_node(node_name)
  320. if lighting_node then
  321. return lighting_node.level
  322. end
  323. end
  324. -- Check if a node name is one of the wielded light nodes
  325. function wielded_light.get_lighting_node(node_name)
  326. return lighting_nodes[node_name]
  327. end
  328. -- Register any node as lightable, register all light level variations for it
  329. function wielded_light.register_lightable_node(node_name, property_overrides, custom_prefix)
  330. -- Node name must be string
  331. if type(node_name) ~= "string" then
  332. error_log("You must provide a node name to be registered as lightable, '%s' given.", type(node_name))
  333. return
  334. end
  335. -- Node must already be registered
  336. local original_definition = minetest.registered_nodes[node_name]
  337. if not original_definition then
  338. error_log("The node '%s' cannot be registered as lightable because it does not exist.", node_name)
  339. return
  340. end
  341. -- Decide the prefix for the lighting node
  342. local prefix = custom_prefix or node_name:gsub(":", "_", 1, true) .. "_"
  343. if lighting_prefixes[prefix] then
  344. error_log("The lighting prefix '%s' cannot be used for '%s' as it is already used for '%s'.", prefix, node_name, lighting_prefixes[prefix])
  345. return
  346. end
  347. lighting_prefixes[prefix] = node_name
  348. -- Default for property overrides
  349. if not property_overrides then property_overrides = {} end
  350. -- Copy the node definition and provide required settings for a lighting node
  351. local new_definition = table.copy(original_definition)
  352. new_definition.on_timer = cleanup_timer_callback
  353. new_definition.paramtype = "light"
  354. new_definition.mod_origin = mod_name
  355. new_definition.groups = new_definition.groups or {}
  356. new_definition.groups.not_in_creative_inventory = 1
  357. -- Make sure original node is dropped if a lit node is dug
  358. if not new_definition.drop then
  359. new_definition.drop = node_name
  360. end
  361. -- Allow any properties to be overridden on registration
  362. for prop, val in pairs(property_overrides) do
  363. new_definition[prop] = val
  364. end
  365. -- If it's a liquid, we need to stop it flowing
  366. if new_definition.groups.liquid then
  367. new_definition.liquid_range = 0
  368. lightable_liquids[node_name] = true
  369. end
  370. -- Register the lighting nodes
  371. lightable_nodes[node_name] = {}
  372. for i=1, minetest.LIGHT_MAX do
  373. local lighting_node_name = wielded_light.lighting_node_of_level(i, prefix)
  374. -- Index for quick finding later
  375. lightable_nodes[node_name][i] = lighting_node_name
  376. lighting_nodes[lighting_node_name] = {
  377. node = node_name,
  378. level = i
  379. }
  380. -- Copy the base definition and apply the light level
  381. local level_definition = table.copy(new_definition)
  382. level_definition.light_source = i
  383. -- If it's a liquid, we need to stop it replacing itself with the original
  384. if level_definition.groups.liquid then
  385. level_definition.liquid_alternative_source = lighting_node_name
  386. level_definition.liquid_alternative_flowing = lighting_node_name
  387. end
  388. minetest.register_node(":"..lighting_node_name, level_definition)
  389. end
  390. end
  391. -- Check if node can have a wielded light node placed in it
  392. function wielded_light.is_lightable_node(node_pos)
  393. local name = minetest.get_node(node_pos).name
  394. if lightable_nodes[name] then
  395. return true
  396. elseif wielded_light.get_lighting_node(name) then
  397. return true
  398. end
  399. return false
  400. end
  401. -- Gets the closest position to pos that's a lightable node
  402. function wielded_light.get_light_position(pos)
  403. local around_vector = {
  404. {x=0, y=0, z=0},
  405. {x=0, y=1, z=0}, {x=0, y=-1, z=0},
  406. {x=1, y=0, z=0}, {x=-1, y=0, z=0},
  407. {x=0, y=0, z=1}, {x=0, y=0, z=-1},
  408. }
  409. for _, around in ipairs(around_vector) do
  410. local light_pos = vector.add(pos, around)
  411. if wielded_light.is_lightable_node(light_pos) then
  412. return light_pos
  413. end
  414. end
  415. end
  416. -- Gets the emitted light level of a given item name
  417. function wielded_light.get_light_def(item_name)
  418. -- Invalid item? No light
  419. if not item_name or item_name == "" then
  420. return 0, false
  421. end
  422. -- If the item is cached return the cached level
  423. local cached_definition = shiny_items[item_name]
  424. if cached_definition then
  425. return cached_definition.level, cached_definition.floodable
  426. end
  427. -- Get the item definition
  428. local stack = ItemStack(item_name)
  429. local itemdef = stack:get_definition()
  430. -- If invalid, no light
  431. if not itemdef then
  432. return 0, false
  433. end
  434. if not minetest.get_item_group(item_name, 'wield_light') then
  435. return 0, false
  436. end
  437. local light_level = minetest.get_item_group(item_name, 'wield_light')
  438. return light_level, itemdef.floodable
  439. end
  440. -- Register an item as shining
  441. function wielded_light.register_item_light(item_name, light_level, floodable)
  442. if shiny_items[item_name] then
  443. if light_level then
  444. shiny_items[item_name].level = light_level
  445. end
  446. if floodable ~= nil then
  447. shiny_items[item_name].floodable = floodable
  448. end
  449. else
  450. if floodable == nil then
  451. local stack = ItemStack(item_name)
  452. local itemdef = stack:get_definition()
  453. floodable = itemdef.floodable
  454. end
  455. shiny_items[item_name] = {
  456. level = light_level,
  457. floodable = floodable or false
  458. }
  459. end
  460. end
  461. -- Mark an item as floodable or not
  462. function wielded_light.register_item_floodable(item_name, floodable)
  463. if floodable == nil then floodable = true end
  464. if shiny_items[item_name] then
  465. shiny_items[item_name].floodable = floodable
  466. else
  467. local calced_level = wielded_light.get_light_def(item_name)
  468. shiny_items[item_name] = {
  469. level = calced_level,
  470. floodable = floodable
  471. }
  472. end
  473. end
  474. -- Keep track of an item entity. Should be called once for an item
  475. function wielded_light.track_item_entity(obj, cat, item)
  476. if not is_entity_valid({ obj=obj }) then return end
  477. local light_level, light_is_floodable = wielded_light.get_light_def(item)
  478. -- If the item does not emit light do not track it
  479. if light_level <= 0 then return end
  480. -- Generate the uid for the item and the id for the light category
  481. local uid = tostring(obj)
  482. local id = get_light_category_id(cat)..uid
  483. -- Create the main tracking object for this item instance if it does not already exist
  484. if not tracked_entities[uid] then
  485. tracked_entities[uid] = { obj=obj, items={}, update = true }
  486. end
  487. -- Create the item tracking object for this item + category
  488. tracked_entities[uid].items[id] = { level=light_level, floodable=light_is_floodable }
  489. -- Add the light in on creation so it's immediate
  490. local pos = entity_pos(obj)
  491. local pos_str = pos and minetest.pos_to_string(pos)
  492. if pos_str then
  493. if not (light_is_floodable and is_lightable_liquid(pos)) then
  494. add_light(pos_str, id, light_level)
  495. end
  496. end
  497. tracked_entities[uid].pos = pos_str
  498. end
  499. -- A player's light should appear near their head not their feet
  500. local player_height_offset = { x=0, y=1, z=0 }
  501. -- Keep track of a user / player entity. Should be called as often as the user updates
  502. function wielded_light.track_user_entity(obj, cat, item)
  503. -- Generate the uid for the player and the id for the light category
  504. local uid = tostring(obj)
  505. local id = get_light_category_id(cat)..uid
  506. -- Create the main tracking object for this player instance if it does not already exist
  507. if not tracked_entities[uid] then
  508. tracked_entities[uid] = { obj=obj, items={}, offset = player_height_offset, update = true }
  509. end
  510. local tracked_entity = tracked_entities[uid]
  511. local tracked_item = tracked_entity.items[id]
  512. -- If the item being tracked for the player changes, update the item tracking object for this item + category
  513. if not tracked_item or tracked_item.item ~= item then
  514. local light_level, light_is_floodable = wielded_light.get_light_def(item)
  515. tracked_entity.items[id] = { level=light_level, item=item, floodable=light_is_floodable }
  516. tracked_entity.update = true
  517. end
  518. end
  519. -- Setup --
  520. -- Wielded item shining globalstep
  521. minetest.register_globalstep(global_timer_callback)
  522. -- Dropped item on_step override
  523. -- https://github.com/minetest/minetest/issues/6909
  524. local builtin_item = minetest.registered_entities["__builtin:item"]
  525. local item = {
  526. on_step = function(self, dtime, ...)
  527. builtin_item.on_step(self, dtime, ...)
  528. -- Register an item once for tracking
  529. -- If it's already being tracked, exit
  530. if self.wielded_light then return end
  531. self.wielded_light = true
  532. local stack = ItemStack(self.itemstring)
  533. local item_name = stack:get_name()
  534. wielded_light.track_item_entity(self.object, "item", item_name)
  535. end
  536. }
  537. setmetatable(item, {__index = builtin_item})
  538. minetest.register_entity(":__builtin:item", item)
  539. -- Track a player's wielded item
  540. wielded_light.register_player_lightstep(function (player)
  541. wielded_light.track_user_entity(player, "wield", player:get_wielded_item():get_name())
  542. end)
  543. wielded_light.register_lightable_node("air", nil, "")
  544. --wielded_light.register_lightable_node(water_name, nil, "water_")
  545. --wielded_light.register_lightable_node("default:river_water_source", nil, "river_water_")
  546. ---TEST
  547. --wielded_light.register_item_light('default:dirt', 14)