1 local minetest,vector,math = minetest,vector,math
2 local weather_channel = minetest.mod_channel_join("weather_type")
3 local weather_intake = minetest.mod_channel_join("weather_intake")
4 local weather_nodes_channel = minetest.mod_channel_join("weather_nodes")
7 weather_channel:send_all("")
8 weather_intake:send_all("")
9 weather_nodes_channel:send_all("")
12 local mod_storage = minetest.get_mod_storage()
14 weather_type = mod_storage:get_int("weather_type")
16 local path = minetest.get_modpath(minetest.get_current_modname())
17 dofile(path.."/commands.lua")
20 --this updates players skys since it cannot be done clientside
21 update_player_sky = function()
22 for _,player in ipairs(minetest.get_connected_players()) do
23 if weather_type ~= 0 then
30 dawn_horizon = "#808080",
32 fog_sun_tint = "#808080",
35 night_horizon="#808080"
37 player:set_sun({visible=false,sunrise_visible=false})
38 player:set_moon({visible=false})
39 player:set_stars({visible=false})
48 dawn_horizon = "#bac1f0",
52 night_horizon="#4090ff"
55 player:set_sun({visible=true,sunrise_visible=true})
56 player:set_moon({visible=true})
57 player:set_stars({visible=true})
62 --this tells the client mod to update the weather type
63 function_send_weather_type = function()
64 weather_channel:send_all(tostring(weather_type))
69 minetest.register_on_mods_loaded(function()
70 for name in pairs(minetest.registered_items) do
71 if name ~= "air" and name ~= "ignore" then
72 table.insert(all_nodes,name)
77 --this sends the client all nodes that weather can be on top of
80 --have the client send the server the ready signal
81 minetest.register_on_modchannel_message(function(channel_name, sender, message)
82 if channel_name == "weather_intake" then
83 minetest.after(0,function()
84 --print("sending player weather")
85 --for some reason this variable assignment does not work outside the scope of this function
86 local all_nodes_serialized = minetest.serialize(all_nodes)
87 weather_nodes_channel:send_all(all_nodes_serialized)
88 function_send_weather_type()
95 minetest.register_on_joinplayer(function(player)
96 minetest.after(3,function()
97 local all_nodes_serialized = minetest.serialize(all_nodes)
98 weather_nodes_channel:send_all(all_nodes_serialized)
99 function_send_weather_type()
105 local cDoSnow_call_count_for_blanket_coverage = 50 -- how many calls of do_snow() are required for blanket snow coverage
106 local cDoSnow_call_count_for_snowState_catchup = 20 -- how many calls of do_snow() (at most) before weather_snowState will catch up to the pattern on the ground (e.g. if player went somewhere else while it was snowing then came back)
107 local cSnowState_LFSR_taps, cSnowState_LFSR_length = 0x100D, 8191 -- Fizzlefade constants for the shortest maximum length LFSR that can cover an 80 x 80 area (i.e. has a length larger than 6400)
108 local cSnow_length_x = 80 -- (cSnow_length_x * cSnow_length_z) MUST be less than cSnowState_LFSR_length
109 local cSnow_length_y = 80
110 local cSnow_length_z = 80 -- (cSnow_length_x * cSnow_length_z) MUST be less than cSnowState_LFSR_length
111 local snow_area = vector.new(cSnow_length_x, cSnow_length_y, cSnow_length_z)
112 local snow_radius = vector.divide(snow_area, 2)
116 local subber = vector.subtract
117 local adder = vector.add
119 local under_air = minetest.find_nodes_in_area_under_air
120 local round_it = vector.round
121 local n_vec = vector.new
123 local get_light = minetest.get_node_light
124 local g_node = minetest.get_node
131 local r_nodes = minetest.registered_nodes
135 local mass_set = minetest.bulk_set_node
136 local inserter = table.insert
138 local floor, ceil = math.floor, math.ceil
139 local weather_snowState
140 local snowState_iterations_per_call = ceil(cSnowState_LFSR_length / cDoSnow_call_count_for_blanket_coverage)
141 local snowState_max_catchup_per_call = ceil(cSnowState_LFSR_length / cDoSnow_call_count_for_snowState_catchup)
142 local under_air_iterations
144 local lsfr_steps_count
149 local under_air_count
152 local acceptable_drawtypes = {
154 ["glasslike"] = true,
155 ["glasslike_framed"] = true,
156 ["glasslike_framed_optional"] = true,
158 ["allfaces_optional"] = true,
163 function XOR( num1, num2 )
164 -- This XOR function is excerpted from the Bitwise Operations Mod v1.2, by Leslie E. Krause
165 -- which is provided under the MIT License (MIT)
167 -- The MIT License (MIT)
169 -- Copyright (c) 2020, Leslie Krause (leslie@searstower.org)
171 -- Permission is hereby granted, free of charge, to any person obtaining a copy of this
172 -- software and associated documentation files (the "Software"), to deal in the Software
173 -- without restriction, including without limitation the rights to use, copy, modify, merge,
174 -- publish, distribute, sublicense, and/or sell copies of the Software, and to permit
175 -- persons to whom the Software is furnished to do so, subject to the following conditions:
177 -- The above copyright notice and this permission notice shall be included in all copies or
178 -- substantial portions of the Software.
180 -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
181 -- INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
182 -- PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
183 -- FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
184 -- OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
185 -- DEALINGS IN THE SOFTWARE.
188 -- https://opensource.org/licenses/MIT
192 while num1 > 0 or num2 > 0 do
193 local rem1 = num1 % 2
194 local rem2 = num2 % 2
199 num1 = ( num1 - rem1 ) / 2
200 num2 = ( num2 - rem2 ) / 2
207 local function do_snow()
208 if weather_type == 1 then
209 for _,player in ipairs(minetest.get_connected_players()) do
211 --local t0 = minetest.get_us_time()/1000000
213 pos = round_it(player:get_pos())
214 min = subber(pos, snow_radius)
215 max = adder(pos, snow_radius)
217 area_index = under_air(min, max, all_nodes)
218 --local node_search_time = math.ceil((minetest.get_us_time()/1000000 - t0) * 1000)
222 --the highest value is always indexed last in minetest.find_nodes_in_area_under_air,
223 --so all that is needed is to iterate through it backwards and hook into the first
224 --y value on the x and y and ignore the rest
226 for key = #area_index,1,-1 do
227 temp_pos = area_index[key]
228 if not spawn_table[temp_pos.x] then spawn_table[temp_pos.x] = {} end
229 if not spawn_table[temp_pos.x][temp_pos.z] then
230 spawn_table[temp_pos.x][temp_pos.z] = temp_pos.y
231 under_air_count = under_air_count + 1
235 --save old method just in case useful or turns out it's faster after all
236 --for _,index in pairs(area_index) do
237 -- if not spawn_table[index.x] then spawn_table[index.x] = {} end
238 -- if not spawn_table[index.x][index.z] then
239 -- spawn_table[index.x][index.z] = index.y
240 -- elseif spawn_table[index.x][index.z] < index.y then
241 -- spawn_table[index.x][index.z] = index.y
247 under_air_iterations = 0
251 -- "fizzelfade" in the snow with a Linear Feedback Shift Register (LFSR)
252 -- https://fabiensanglard.net/fizzlefade/index.php
253 lsb = weather_snowState % 2 -- Get the output bit.
254 weather_snowState = floor(weather_snowState / 2) -- Shift register
256 weather_snowState = XOR(weather_snowState, cSnowState_LFSR_taps)
258 lsfr_steps_count = lsfr_steps_count + 1
260 location_bits = weather_snowState - 1 -- LFSR values start at 1, but we want snow to be able to fall on (0, 0)
261 relative_x = location_bits % cSnow_length_x
262 relative_z = floor(location_bits / cSnow_length_x)
264 if relative_z < cSnow_length_z then
265 x = (floor(min.x / cSnow_length_x) * cSnow_length_x) + relative_x -- align fizzelfade coords world-global
266 if x < min.x then x = x + cSnow_length_x end -- ensure it falls in the same space as area_index
267 local x_index = spawn_table[x]
268 if x_index ~= nil then
269 z = (floor(min.z / cSnow_length_z) * cSnow_length_z) + relative_z -- align fizzelfade coords world-global
270 if z < min.z then z = z + cSnow_length_z end -- ensure it falls in the same space as area_index
274 -- We hit a location that's in the spawn_table
275 under_air_iterations = under_air_iterations + 1
277 lightlevel = get_light(n_vec(x,y+1,z), 0.5)
278 if lightlevel >= 14 then
279 -- daylight is above or near this node, so snow can fall on it
281 node_name = g_node(n_vec(x,y,z)).name
282 def = r_nodes[node_name]
283 --buildable = def.buildable_to
285 drawtype = acceptable_drawtypes[def.drawtype]
287 walkable = def.walkable
288 liquid = (def.liquidtype ~= "none")
290 if not liquid and walkable and drawtype and node_name ~= "main:ice" then
292 -- if node_name ~= "weather:snow" then
293 -- inserter(bulk_list, n_vec(x,y,z))
295 -- catchup_steps = catchup_steps + 1 -- we've already snowed on this spot
297 --elseif walkable then
298 if g_node(n_vec(x,y+1,z)).name ~= "weather:snow" then
299 inserter(bulk_list, n_vec(x,y+1,z))
301 catchup_steps = catchup_steps + 1 -- we've already snowed on this spot
304 elseif node_name == "main:water" then
305 inserter(ice_list, n_vec(x,y,z))
312 until (lsfr_steps_count - catchup_steps) >= snowState_iterations_per_call or catchup_steps >= snowState_max_catchup_per_call
315 mass_set(bulk_list, {name="weather:snow"})
318 mass_set(ice_list, {name="main:ice"})
324 local chugent = math.ceil((minetest.get_us_time()/1000000 - t0) * 1000)
325 print("---------------------------------")
326 print("find_nodes_in_area_under_air() time: " .. node_search_time .. " ms")
327 print("New Snow generation time: " .. chugent .. " ms [" .. (chugent - node_search_time) .. " ms]")
329 inserter(average, chugent)
331 --don't cause memory leak
332 if get_table_size(average) > 10 then
333 table.remove(average,1)
335 for _,i in ipairs(average) do
339 a = a / get_table_size(average)
340 print("average = "..a.."ms")
341 minetest.chat_send_all("total nodes under air: " .. under_air_count .. ", LFSR iterations: " .. lsfr_steps_count .. ", under-air hits (nodes tested): " .. under_air_iterations .. " Snow added: " .. (#bulk_list + #ice_list) .. ", snow already there (catchup): " .. catchup_steps)
342 --print("---------------------------------")
347 minetest.after(3, function()
351 minetest.register_on_mods_loaded(function()
352 minetest.after(0,function()
359 --this sets random weather
360 local initial_run = true
362 local function randomize_weather()
363 if not initial_run then
364 new_weather = math.random(0,weather_max)
365 if new_weather ~= weather_type or not weather_type then
366 weather_type = new_weather
370 mod_storage:set_int("weather_type", weather_type)
375 function_send_weather_type()
378 minetest.after((math.random(15,20)+math.random())*60, function()
383 minetest.register_on_mods_loaded(function()
384 minetest.after(0,function()
385 if mod_storage:get_int("weather_initialized") == 0 then
386 mod_storage:set_int("weather_initialized",1)
387 weather_type = math.random(0,weather_max)
388 mod_storage:set_int("weather_type", weather_type)
391 weather_snowState = math.max(mod_storage:get_int("weather_snowState"), 1)
397 minetest.register_on_shutdown(function()
398 mod_storage:set_int("weather_type", weather_type)
399 mod_storage:set_int("weather_snowState", weather_snowState)
402 local snowball_throw = function(player)
403 local pos = player:get_pos()
404 pos.y = pos.y + 1.625
405 --let other players hear the noise too
406 minetest.sound_play("woosh",{to_player=player:get_player_name(), pitch = math.random(80,100)/100})
407 minetest.sound_play("woosh",{pos=pos, exclude_player = player:get_player_name(), pitch = math.random(80,100)/100})
408 local snowball = minetest.add_entity(pos,"weather:snowball")
410 local vel = player:get_player_velocity()
411 snowball:set_velocity(vector.add(vel,vector.multiply(player:get_look_dir(),20)))
412 snowball:get_luaentity().thrower = player:get_player_name()
418 minetest.register_node("weather:snow", {
419 description = "Snow",
420 tiles = {"snow.png"},
421 groups = {pathable = 1,snow = 1, falling_node=1},
422 sounds = main.woolSound(),
424 drawtype = "nodebox",
431 items = {"weather:snowball"},
434 items = {"weather:snowball"},
437 items = {"weather:snowball"},
440 items = {"weather:snowball"},
444 items = {"weather:snowball"},
452 {-8/16, -8/16, -8/16, 8/16, -6/16, 8/16},
457 minetest.register_node("weather:snow_block", {
458 description = "Snow",
459 tiles = {"snow.png"},
460 groups = {pathable = 1,snow = 1},
461 sounds = main.woolSound(),
466 items = {"weather:snowball"},
469 items = {"weather:snowball"},
472 items = {"weather:snowball"},
475 items = {"weather:snowball"},
479 items = {"weather:snowball"},
485 minetest.register_abm({
486 label = "snow and ice melt",
487 nodenames = {"weather:snow","main:ice"},
492 action = function(pos)
493 if weather_type ~= 1 then
494 minetest.remove_node(pos)
499 minetest.register_craftitem("weather:snowball", {
500 description = "Snowball",
501 inventory_image = "snowball.png",
504 on_place = function(itemstack, placer, pointed_thing)
505 local worked = snowball_throw(placer)
507 itemstack:take_item()
511 on_secondary_use = function(itemstack, user, pointed_thing)
512 local worked = snowball_throw(user)
514 itemstack:take_item()
522 snowball.initial_properties = {
525 collide_with_objects = false,
526 collisionbox = {-0.1, -0.1, -0.1, 0.1, 0.1, 0.1},
528 visual_size = {x = 0.5, y = 0.5},
536 snowball.snowball = true
538 snowball.on_activate = function(self)
539 self.object:set_acceleration(vector.new(0,-9.81,0))
542 --make this as efficient as possible
543 --make it so you can hit one snowball with another
544 snowball.on_step = function(self, dtime)
545 local vel = self.object:get_velocity()
547 local pos = self.object:get_pos()
549 --hit object with the snowball
550 for _,object in ipairs(minetest.get_objects_inside_radius(pos, 1)) do
551 if (object:is_player() and object:get_hp() > 0 and object:get_player_name() ~= self.thrower) or (object:get_luaentity() and object:get_luaentity().mob == true and object ~= self.owner) then
552 object:punch(self.object, 2,
554 full_punch_interval=1.5,
555 damage_groups = {damage=0,fleshy=0},
562 if (self.oldvel and ((vel.x == 0 and self.oldvel.x ~= 0) or (vel.y == 0 and self.oldvel.y ~= 0) or (vel.z == 0 and self.oldvel.z ~= 0))) or hit == true then
563 --snowballs explode in the nether
564 if pos.y <= -10033 and pos.y >= -20000 then
568 minetest.sound_play("wool",{pos=pos, pitch = math.random(80,100)/100})
569 minetest.add_particlespawner({
571 -- Number of particles spawned over the time period `time`.
574 -- Lifespan of spawner in seconds.
575 -- If time is 0 spawner has infinite lifespan and spawns the `amount` on
576 -- a per-second basis.
580 minvel = {x=-2, y=3, z=-2},
581 maxvel = {x=2, y=5, z=2},
582 minacc = {x=0, y=-9.81, z=0},
583 maxacc = {x=0, y=-9.81, z=0},
588 -- The particles' properties are random values between the min and max
590 -- pos, velocity, acceleration, expirationtime, size
592 collisiondetection = true,
594 collision_removal = true,
596 object_collision = false,
598 texture = "snowflake_"..math.random(1,2)..".png",
607 minetest.register_entity("weather:snowball", snowball)