diff --git a/mods/PLAYER/wielded_light/init.lua b/mods/PLAYER/wielded_light/init.lua index 94d2e2a..970b3ec 100644 --- a/mods/PLAYER/wielded_light/init.lua +++ b/mods/PLAYER/wielded_light/init.lua @@ -1,136 +1,647 @@ +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.3 + +-- 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 = {} ---- Shining API --- -wielded_light = {} +-- List of custom callbacks for each update step +local update_callbacks = {} +local update_player_callbacks = {} -function wielded_light.update_light(pos, light_level) - 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}, +-- 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 } - local do_update = false - local old_value = 0 - local timer - local light_pos - for _, around in ipairs(around_vector) do - light_pos = vector.add(pos, around) - local name = minetest.get_node(light_pos).name - if name == "air" and (minetest.get_node_light(light_pos) or 0) < light_level then - do_update = true - break - elseif name:sub(1,13) == "wielded_light" then -- Update existing light node and timer - old_value = tonumber(name:sub(15)) - if light_level > old_value then - do_update = true - else - timer = minetest.get_node_timer(light_pos) - local elapsed = timer:get_elapsed() - if elapsed > (update_interval * 1.5) then - -- The timer is set to 3x update_interval - -- This node was not updated the last interval and may - -- is disabled before the next step - -- Therefore the light should be re-set to avoid flicker - do_update = true - end - end - break + }, + 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 do_update then - timer = timer or minetest.get_node_timer(light_pos) - if light_level ~= old_value then - minetest.swap_node(light_pos, {name = "wielded_light:"..light_level}) - end - timer:start(update_interval*3) + -- 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 -function wielded_light.update_light_by_item(item, pos) - local stack = ItemStack(item) - local light_level = shiny_items[stack:get_name()] - local itemdef = stack:get_definition() - if not light_level and not itemdef then +-- 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 - light_level = light_level or ((itemdef.light_source or 0) - level_delta) + -- If no light in this position remove any light node + if max_light == 0 then + reset_lighting_node(pos_vec) + return + end - if light_level > 0 then - wielded_light.update_light(pos, light_level) + -- 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 -function wielded_light.register_item_light(itemname, light_level) - shiny_items[itemname] = light_level -end - - --- Register helper nodes -for i=1, 14 do - minetest.register_node("wielded_light:"..i, { - drawtype = "airlike", - groups = {not_in_creative_inventory = 1}, - walkable = false, - paramtype = "light", - sunlight_propagates = true, - light_source = i, - pointable = false, - buildable_to = true, - drop = {}, - on_timer = function(pos, elapsed) - minetest.swap_node(pos, {name = "air"}) - end, - }) -end - --- Wielded item shining globalstep local timer = 0 -minetest.register_globalstep(function(dtime) +-- 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 - for _, player in pairs(minetest.get_connected_players()) do - -- predict where the player will be the next time we place the light - -- assume that on average we're slightly past 1/2 of the next interval, hence 1.5 - -- (since the scheduling is a bit behind) - -- experimentally this also works nicely - local pos = vector.add ( - vector.add({x = 0, y = 1, z = 0}, vector.round(player:get_pos())), - vector.round(vector.multiply(player:get_player_velocity(), update_interval * 1.5)) - ) - - wielded_light.update_light_by_item(player:get_wielded_item(), pos) + -- 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 -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 + + -- Get the light level of an item from its definition + -- Reduce the light level by level_delta - original functionality + -- Limit between 0 and the max light level + return math.min(math.max((itemdef.light_source or 0) - level_delta, 0), minetest.LIGHT_MAX), 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, moveresult) - builtin_item.on_step(self, dtime, moveresult) - - self.shining_timer = (self.shining_timer or 0) + dtime - if self.shining_timer >= update_interval then - self.shining_timer = 0 - local pos = self.object:get_pos() - if pos then - wielded_light.update_light_by_item(self.itemstring, pos) - end - end + 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) + +-- Register helper nodes +local water_name = "default:water_source" +if minetest.get_modpath("hades_core") then + water_name = "hades_core:water_source" +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) + +--do mesecraft's nodes +dofile(minetest.get_modpath("wielded_light").."/mesecraft.lua") diff --git a/mods/PLAYER/wielded_light/mesecraft.lua b/mods/PLAYER/wielded_light/mesecraft.lua new file mode 100644 index 0000000..67e64b6 --- /dev/null +++ b/mods/PLAYER/wielded_light/mesecraft.lua @@ -0,0 +1,44 @@ +-- mesecraft lit node registration + +local rln = wielded_light.register_lightable_node +minetest.register_on_mods_loaded(function() + rln("default:ladder_wood", nil, "ladder_wood_") + rln("default:ladder_steel", nil, "ladder_steel_") + rln("mtg_plus:ladder_papyrus", nil, "ladder_papyrus_") + rln("df_trees:spore_tree_ladder", nil, "ladder_sporetree_") + rln("ropes:ropeladder_top", nil, "ladder_ropetop_") + rln("ropes:ropeladder", nil, "ladder_ropemid_") + rln("ropes:ropeladder_bottom", nil, "ladder_ropebottom_") + rln("moreblocks:rope", nil, "mb_rope_") + + + local mtgplus = { + "gold", + "bronze", + "copper", + "tin", + "aspen_wood", + "acacia_wood", + "pine_wood", + "junglewood", + } + + for i = 1, #mtgplus do + rln("mtg_plus:ladder_"..mtgplus[i], nil, "ladder_"..mtgplus[i].."_") + end + local rtype = { + "wood", + "copper", + "steel", + } + for i = 1,#rtype do + for len = 1,9 do + local name = "ropes:"..rtype[i]..len.."rope_block" + if minetest.registered_nodes[name] then + rln(name, nil, rtype[i]..len.."ropeblock_") + end + end + end + rln("ropes:rope_bottom", nil , "rope_bottom_") + rln("ropes:rope", nil , "rope_") +end) diff --git a/mods/PLAYER/wielded_light/mod.conf b/mods/PLAYER/wielded_light/mod.conf index 8f3256a..2efb378 100644 --- a/mods/PLAYER/wielded_light/mod.conf +++ b/mods/PLAYER/wielded_light/mod.conf @@ -1 +1,2 @@ name = wielded_light +optional_depends = default, hades_core