-
Notifications
You must be signed in to change notification settings - Fork 11
/
init.lua
646 lines (553 loc) · 19.7 KB
/
init.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
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
-- 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, ...)
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
wielded_light.register_lightable_node("air", nil, "")
if minetest.get_modpath("default") then
wielded_light.register_lightable_node("default:water_source", nil, "water_")
wielded_light.register_lightable_node("default:river_water_source", nil, "river_water_")
elseif minetest.get_modpath("hades_core") then
wielded_light.register_lightable_node("hades_core:water_source", nil, "water_")
else
error_log("Not running in a supported game, lightable water disabled")
end
---TEST
--wielded_light.register_item_light('default:dirt', 14)