123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640 |
- local mod_name = minetest.get_current_modname()
- -- Node replacements that emit light
- -- Sets of lighting_node={ node=original_node, level=light_level }
- local lighting_nodes = {}
- -- The nodes that can be replaced with lighting nodes
- -- Sets of original_node={ [1]=lighting_node_1, [2]=lighting_node_2, ... }
- local lightable_nodes = {}
- -- Prefixes used for each node so we can avoid overlap
- -- Pairs of prefix=original_node
- local lighting_prefixes = {}
- -- node_name=true pairs of lightable nodes that are liquids and can flood some light sources
- local lightable_liquids = {}
- -- How often will the positions of lights be recalculated
- local update_interval = 0.2
- -- How long until a previously lit node should be updated - reduces flicker
- local removal_delay = update_interval * 0.5
- -- How often will a node attempt to check itself for deletion
- local cleanup_interval = update_interval * 3
- -- How far in the future will the position be projected based on the velocity
- local velocity_projection = update_interval * 1
- -- How many light levels should an item held in the hand be reduced by, compared to the placed node
- -- does not apply to manually registered light levels
- local level_delta = 2
- -- item=light_level pairs of registered wielded lights
- local shiny_items = {}
- -- List of custom callbacks for each update step
- local update_callbacks = {}
- local update_player_callbacks = {}
- -- position={id=light_level} sets of known about light sources and their levels by position
- local active_lights = {}
- --[[ Sets of entities being tracked, in the form:
- entity_id = {
- obj = entity,
- items = {
- category_id..entity_id = {
- level = light_level,
- item? = item_name
- }
- },
- update = true | false,
- pos? = position_vector,
- offset? = offset_vector,
- }
- ]]
- local tracked_entities = {}
- -- position=true pairs of positions that need to be recaculated this update step
- local light_recalcs = {}
- --[[
- Using 2-digit hex codes for categories
- Starts at 00, ends at FF
- This makes it easier extract `uid` from `cat_id..uid` by slicing off 2 characters
- The category ID must be of a fixed length (2 characters)
- ]]
- local cat_id = 0
- local cat_codes = {}
- local function get_light_category_id(cat)
- -- If the category id does not already exist generate a new one
- if not cat_codes[cat] then
- if cat_id >= 256 then
- error("Wielded item category limit exceeded, maximum 256 wield categories")
- end
- local code = string.format("%02x", cat_id)
- cat_id = cat_id+1
- cat_codes[cat] = code
- end
- -- If the category id does exist, return it
- return cat_codes[cat]
- end
- -- Log an error coming from this mod
- local function error_log(message, ...)
- minetest.log("error", "[Wielded Light] " .. (message:format(...)))
- end
- -- Is a node lightable and a liquid capable of flooding some light sources
- local function is_lightable_liquid(pos)
- local node = minetest.get_node_or_nil(pos)
- if not node then return end
- return lightable_liquids[node.name]
- end
- -- Check if an entity instance still exists in the world
- local function is_entity_valid(entity)
- return entity and (entity.obj:is_player() or (entity.obj:get_luaentity() and entity.obj:get_luaentity().name) or false)
- end
- -- Check whether a node was registered by the wield_light mod
- local function is_wieldlight_node(pos_vec)
- local name = string.sub(minetest.get_node(pos_vec).name, 1, #mod_name)
- return name == mod_name
- end
- -- Get the projected position of an entity based on its velocity, rounded to the nearest block
- local function entity_pos(obj, offset)
- local velocity
- if (minetest.features.direct_velocity_on_players or not obj:is_player()) and obj.get_velocity then
- velocity = obj:get_velocity()
- else
- velocity = obj:get_player_velocity()
- end
- return wielded_light.get_light_position(
- vector.round(
- vector.add(
- vector.add(
- offset or { x=0, y=0, z=0 },
- obj:get_pos()
- ),
- vector.multiply(
- velocity or { x=0, y=0, z=0 },
- velocity_projection
- )
- )
- )
- )
- end
- -- Add light to active light list and mark position for update
- local function add_light(pos, id, light_level)
- if not active_lights[pos] then
- active_lights[pos] = {}
- end
- if active_lights[pos][id] ~= light_level then
- -- minetest.log("error", "add "..id.." "..pos.." "..tostring(light_level))
- active_lights[pos][id] = light_level
- light_recalcs[pos] = true
- end
- end
- -- Remove light from active light list and mark position for update
- local function remove_light(pos, id)
- if not active_lights[pos] then return end
- -- minetest.log("error", "rem "..id.." "..pos)
- active_lights[pos][id] = nil
- minetest.after(removal_delay, function ()
- light_recalcs[pos] = true
- end)
- end
- -- Track an entity's position and update its light, will be called on every update step
- local function update_entity(entity)
- local pos = entity_pos(entity.obj, entity.offset)
- local pos_str = pos and minetest.pos_to_string(pos)
- -- If the position has changed, remove the old light and mark the entity for update
- if entity.pos and pos_str ~= entity.pos then
- entity.update = true
- for id,_ in pairs(entity.items) do
- remove_light(entity.pos, id)
- end
- end
- -- Update the recorded position
- entity.pos = pos_str
- -- If the position is still loaded, pump the timer up so it doesn't get removed
- if pos then
- -- If the entity is marked for an update, add the light in the position if it emits light
- if entity.update then
- for id, item in pairs(entity.items) do
- if item.level > 0 and not (item.floodable and is_lightable_liquid(pos)) then
- add_light(pos_str, id, item.level)
- else
- remove_light(pos_str, id)
- end
- end
- end
- end
- if active_lights[pos_str] then
- if is_wieldlight_node(pos) then
- minetest.get_node_timer(pos):start(cleanup_interval)
- end
- end
- entity.update = false
- end
- -- Save the original nodes timer if it has one
- local function save_timer(pos_vec)
- local timer = minetest.get_node_timer(pos_vec)
- if timer:is_started() then
- local meta = minetest.get_meta(pos_vec)
- meta:set_float("saved_timer_timeout", timer:get_timeout())
- meta:set_float("saved_timer_elapsed", timer:get_elapsed())
- end
- end
- -- Restore the original nodes timer if it had one
- local function restore_timer(pos_vec)
- local meta = minetest.get_meta(pos_vec)
- local timeout = meta:get_float("saved_timer_timeout")
- if timeout > 0 then
- local elapsed = meta:get_float("saved_timer_elapsed")
- local timer = minetest.get_node_timer(pos_vec)
- timer:set(timeout, elapsed)
- meta:set_string("saved_timer_timeout","")
- meta:set_string("saved_timer_elapsed","")
- end
- end
- -- Replace a lighting node with its original counterpart
- local function reset_lighting_node(pos)
- local existing_node = minetest.get_node(pos)
- local lighting_node = wielded_light.get_lighting_node(existing_node.name)
- if not lighting_node then
- return
- end
- minetest.swap_node(pos, { name = lighting_node.node,param2 = existing_node.param2 })
- restore_timer(pos)
- end
- -- Will be run once the node timer expires
- local function cleanup_timer_callback(pos, elapsed)
- local pos_str = minetest.pos_to_string(pos)
- local lights = active_lights[pos_str]
- -- If no active lights for this position, remove itself
- if not lights then
- reset_lighting_node(pos)
- else
- -- Clean up any tracked entities for this position that no longer exist
- for id,_ in pairs(lights) do
- local uid = string.sub(id,3)
- local entity = tracked_entities[uid]
- if not is_entity_valid(entity) then
- remove_light(pos_str, id)
- end
- end
- minetest.get_node_timer(pos):start(cleanup_interval)
- end
- end
- -- Recalculate the total light level for a given position and update the light level there
- local function recalc_light(pos)
- -- If not in active lights list we can't do anything
- if not active_lights[pos] then return end
- -- Calculate the light level of the node
- local any_light = false
- local max_light = 0
- for id, light_level in pairs(active_lights[pos]) do
- any_light = true
- if light_level > max_light then
- max_light = light_level
- end
- end
- -- Convert the position back to a vector
- local pos_vec = minetest.string_to_pos(pos)
- -- If no items in this position, delete it from the list and remove any light node
- if not any_light then
- active_lights[pos] = nil
- reset_lighting_node(pos_vec)
- return
- end
- -- If no light in this position remove any light node
- if max_light == 0 then
- reset_lighting_node(pos_vec)
- return
- end
- -- Limit the light level
- max_light = math.min(max_light, minetest.LIGHT_MAX)
- -- Get the current light level in this position
- local existing_node = minetest.get_node(pos_vec)
- local name = existing_node.name
- local old_value = wielded_light.level_of_lighting_node(name) or 0
- -- If the light level has changed, set the coresponding light node and initiate the cleanup timer
- if old_value ~= max_light then
- local node_name
- if lightable_nodes[name] then
- node_name = name
- elseif lighting_nodes[name] then
- node_name = lighting_nodes[name].node
- end
- if node_name then
- if not is_wieldlight_node(pos_vec) then
- save_timer(pos_vec)
- end
- minetest.swap_node(pos_vec, {
- name = lightable_nodes[node_name][max_light],
- param2 = existing_node.param2
- })
- minetest.get_node_timer(pos_vec):start(cleanup_interval)
- else
- active_lights[pos] = nil
- end
- end
- end
- local timer = 0
- -- Will be run on every global step
- local function global_timer_callback(dtime)
- -- Only run once per update interval, global step will be called much more often than that
- timer = timer + dtime;
- if timer < update_interval then
- return
- end
- timer = 0
- -- Run all custom player callbacks for each player
- local connected_players = minetest.get_connected_players()
- for _,callback in pairs(update_player_callbacks) do
- for _, player in pairs(connected_players) do
- callback(player)
- end
- end
- -- Run all custom callbacks
- for _,callback in pairs(update_callbacks) do
- callback()
- end
- -- Look at each tracked entity and update its position
- for uid, entity in pairs(tracked_entities) do
- if is_entity_valid(entity) then
- update_entity(entity)
- else
- -- If the entity no longer exists, stop tracking it
- tracked_entities[uid] = nil
- end
- end
- -- Recalculate light levels
- for pos,_ in pairs(light_recalcs) do
- recalc_light(pos)
- end
- light_recalcs = {}
- end
- --- Shining API ---
- wielded_light = {}
- -- Registers a callback to be called every time the update interval is passed
- function wielded_light.register_lightstep(callback)
- table.insert(update_callbacks, callback)
- end
- -- Registers a callback to be called for each player every time the update interval is passed
- function wielded_light.register_player_lightstep(callback)
- table.insert(update_player_callbacks, callback)
- end
- -- Returns the node name for a given light level
- function wielded_light.lighting_node_of_level(light_level, prefix)
- return mod_name..":"..(prefix or "")..light_level
- end
- -- Gets the light level for a given node name, inverse of lighting_node_of_level
- function wielded_light.level_of_lighting_node(node_name)
- local lighting_node = wielded_light.get_lighting_node(node_name)
- if lighting_node then
- return lighting_node.level
- end
- end
- -- Check if a node name is one of the wielded light nodes
- function wielded_light.get_lighting_node(node_name)
- return lighting_nodes[node_name]
- end
- -- Register any node as lightable, register all light level variations for it
- function wielded_light.register_lightable_node(node_name, property_overrides, custom_prefix)
- -- Node name must be string
- if type(node_name) ~= "string" then
- error_log("You must provide a node name to be registered as lightable, '%s' given.", type(node_name))
- return
- end
- -- Node must already be registered
- local original_definition = minetest.registered_nodes[node_name]
- if not original_definition then
- error_log("The node '%s' cannot be registered as lightable because it does not exist.", node_name)
- return
- end
- -- Decide the prefix for the lighting node
- local prefix = custom_prefix or node_name:gsub(":", "_", 1, true) .. "_"
- if lighting_prefixes[prefix] then
- error_log("The lighting prefix '%s' cannot be used for '%s' as it is already used for '%s'.", prefix, node_name, lighting_prefixes[prefix])
- return
- end
- lighting_prefixes[prefix] = node_name
- -- Default for property overrides
- if not property_overrides then property_overrides = {} end
- -- Copy the node definition and provide required settings for a lighting node
- local new_definition = table.copy(original_definition)
- new_definition.on_timer = cleanup_timer_callback
- new_definition.paramtype = "light"
- new_definition.mod_origin = mod_name
- new_definition.groups = new_definition.groups or {}
- new_definition.groups.not_in_creative_inventory = 1
- -- Make sure original node is dropped if a lit node is dug
- if not new_definition.drop then
- new_definition.drop = node_name
- end
- -- Allow any properties to be overridden on registration
- for prop, val in pairs(property_overrides) do
- new_definition[prop] = val
- end
- -- If it's a liquid, we need to stop it flowing
- if new_definition.groups.liquid then
- new_definition.liquid_range = 0
- lightable_liquids[node_name] = true
- end
- -- Register the lighting nodes
- lightable_nodes[node_name] = {}
- for i=1, minetest.LIGHT_MAX do
- local lighting_node_name = wielded_light.lighting_node_of_level(i, prefix)
- -- Index for quick finding later
- lightable_nodes[node_name][i] = lighting_node_name
- lighting_nodes[lighting_node_name] = {
- node = node_name,
- level = i
- }
- -- Copy the base definition and apply the light level
- local level_definition = table.copy(new_definition)
- level_definition.light_source = i
- -- If it's a liquid, we need to stop it replacing itself with the original
- if level_definition.groups.liquid then
- level_definition.liquid_alternative_source = lighting_node_name
- level_definition.liquid_alternative_flowing = lighting_node_name
- end
- minetest.register_node(":"..lighting_node_name, level_definition)
- end
- end
- -- Check if node can have a wielded light node placed in it
- function wielded_light.is_lightable_node(node_pos)
- local name = minetest.get_node(node_pos).name
- if lightable_nodes[name] then
- return true
- elseif wielded_light.get_lighting_node(name) then
- return true
- end
- return false
- end
- -- Gets the closest position to pos that's a lightable node
- function wielded_light.get_light_position(pos)
- local around_vector = {
- {x=0, y=0, z=0},
- {x=0, y=1, z=0}, {x=0, y=-1, z=0},
- {x=1, y=0, z=0}, {x=-1, y=0, z=0},
- {x=0, y=0, z=1}, {x=0, y=0, z=-1},
- }
- for _, around in ipairs(around_vector) do
- local light_pos = vector.add(pos, around)
- if wielded_light.is_lightable_node(light_pos) then
- return light_pos
- end
- end
- end
- -- Gets the emitted light level of a given item name
- function wielded_light.get_light_def(item_name)
- -- Invalid item? No light
- if not item_name or item_name == "" then
- return 0, false
- end
- -- If the item is cached return the cached level
- local cached_definition = shiny_items[item_name]
- if cached_definition then
- return cached_definition.level, cached_definition.floodable
- end
- -- Get the item definition
- local stack = ItemStack(item_name)
- local itemdef = stack:get_definition()
- -- If invalid, no light
- if not itemdef then
- return 0, false
- end
- if not minetest.get_item_group(item_name, 'wield_light') then
- return 0, false
- end
- local light_level = minetest.get_item_group(item_name, 'wield_light')
- return light_level, itemdef.floodable
- end
- -- Register an item as shining
- function wielded_light.register_item_light(item_name, light_level, floodable)
- if shiny_items[item_name] then
- if light_level then
- shiny_items[item_name].level = light_level
- end
- if floodable ~= nil then
- shiny_items[item_name].floodable = floodable
- end
- else
- if floodable == nil then
- local stack = ItemStack(item_name)
- local itemdef = stack:get_definition()
- floodable = itemdef.floodable
- end
- shiny_items[item_name] = {
- level = light_level,
- floodable = floodable or false
- }
- end
- end
- -- Mark an item as floodable or not
- function wielded_light.register_item_floodable(item_name, floodable)
- if floodable == nil then floodable = true end
- if shiny_items[item_name] then
- shiny_items[item_name].floodable = floodable
- else
- local calced_level = wielded_light.get_light_def(item_name)
- shiny_items[item_name] = {
- level = calced_level,
- floodable = floodable
- }
- end
- end
- -- Keep track of an item entity. Should be called once for an item
- function wielded_light.track_item_entity(obj, cat, item)
- if not is_entity_valid({ obj=obj }) then return end
- local light_level, light_is_floodable = wielded_light.get_light_def(item)
- -- If the item does not emit light do not track it
- if light_level <= 0 then return end
- -- Generate the uid for the item and the id for the light category
- local uid = tostring(obj)
- local id = get_light_category_id(cat)..uid
- -- Create the main tracking object for this item instance if it does not already exist
- if not tracked_entities[uid] then
- tracked_entities[uid] = { obj=obj, items={}, update = true }
- end
- -- Create the item tracking object for this item + category
- tracked_entities[uid].items[id] = { level=light_level, floodable=light_is_floodable }
- -- Add the light in on creation so it's immediate
- local pos = entity_pos(obj)
- local pos_str = pos and minetest.pos_to_string(pos)
- if pos_str then
- if not (light_is_floodable and is_lightable_liquid(pos)) then
- add_light(pos_str, id, light_level)
- end
- end
- tracked_entities[uid].pos = pos_str
- end
- -- A player's light should appear near their head not their feet
- local player_height_offset = { x=0, y=1, z=0 }
- -- Keep track of a user / player entity. Should be called as often as the user updates
- function wielded_light.track_user_entity(obj, cat, item)
- -- Generate the uid for the player and the id for the light category
- local uid = tostring(obj)
- local id = get_light_category_id(cat)..uid
- -- Create the main tracking object for this player instance if it does not already exist
- if not tracked_entities[uid] then
- tracked_entities[uid] = { obj=obj, items={}, offset = player_height_offset, update = true }
- end
- local tracked_entity = tracked_entities[uid]
- local tracked_item = tracked_entity.items[id]
- -- If the item being tracked for the player changes, update the item tracking object for this item + category
- if not tracked_item or tracked_item.item ~= item then
- local light_level, light_is_floodable = wielded_light.get_light_def(item)
- tracked_entity.items[id] = { level=light_level, item=item, floodable=light_is_floodable }
- tracked_entity.update = true
- end
- end
- -- Setup --
- -- Wielded item shining globalstep
- minetest.register_globalstep(global_timer_callback)
- -- Dropped item on_step override
- -- https://github.com/minetest/minetest/issues/6909
- local builtin_item = minetest.registered_entities["__builtin:item"]
- local item = {
- on_step = function(self, dtime, ...)
- builtin_item.on_step(self, dtime, ...)
- -- Register an item once for tracking
- -- If it's already being tracked, exit
- if self.wielded_light then return end
- self.wielded_light = true
- local stack = ItemStack(self.itemstring)
- local item_name = stack:get_name()
- wielded_light.track_item_entity(self.object, "item", item_name)
- end
- }
- setmetatable(item, {__index = builtin_item})
- minetest.register_entity(":__builtin:item", item)
- -- Track a player's wielded item
- wielded_light.register_player_lightstep(function (player)
- wielded_light.track_user_entity(player, "wield", player:get_wielded_item():get_name())
- end)
- wielded_light.register_lightable_node("air", nil, "")
- --wielded_light.register_lightable_node(water_name, nil, "water_")
- --wielded_light.register_lightable_node("default:river_water_source", nil, "river_water_")
- ---TEST
- --wielded_light.register_item_light('default:dirt', 14)
|