Update to current wielded_light, add all ladders as lit nodes

master
Jeremy 2022-07-27 22:52:49 -07:00
parent d4e504fd1a
commit 1ca2cca2dd
3 changed files with 652 additions and 96 deletions

View File

@ -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")

View File

@ -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)

View File

@ -1 +1,2 @@
name = wielded_light
optional_depends = default, hades_core