]> git.lizzy.rs Git - Crafter.git/blobdiff - mods/weather/init.lua
Added Seasons
[Crafter.git] / mods / weather / init.lua
index c6a1ab241ab1c5876c73f25d87a9917e55c8b701..27f8a67d8c1561861af738910a45276378114196 100644 (file)
@@ -1,23 +1,36 @@
+local minetest,vector,math = minetest,vector,math
+local weather_channel = minetest.mod_channel_join("weather_type")
+local weather_intake = minetest.mod_channel_join("weather_intake")
+local weather_nodes_channel = minetest.mod_channel_join("weather_nodes")
+
+
+weather_channel:send_all("")
+weather_intake:send_all("")
+weather_nodes_channel:send_all("")
+
 local weather_max = 2
-local weather_type = math.random(0,weather_max)
-local weather_timer = 0
+local mod_storage = minetest.get_mod_storage()
+
+weather_type = mod_storage:get_int("weather_type")
 
+local path = minetest.get_modpath(minetest.get_current_modname())
+dofile(path.."/commands.lua")
 
 
 --this updates players skys since it cannot be done clientside
-local update_player_sky = function()
+update_player_sky = function()
        for _,player in ipairs(minetest.get_connected_players()) do
                if weather_type ~= 0 then
                        player:set_sky({
                                base_color="#808080",
                                type="plain",
                                clouds=false,
-                               
+
                                day_sky = "#808080",
                                dawn_horizon = "#808080",
                                dawn_sky = "#808080",
                                fog_sun_tint = "#808080",
-                               
+
                                night_sky="#808080",
                                night_horizon="#808080"
                        })
@@ -29,16 +42,16 @@ local update_player_sky = function()
                                base_color="#8cbafa",
                                type="regular",
                                clouds=true,
-                               
+
                                day_sky = "#8cbafa",
-                               
+
                                dawn_horizon = "#bac1f0",
                                dawn_sky = "#b4bafa",
-                               
+
                                night_sky="#006aff",
                                night_horizon="#4090ff"
                        })
-                       
+
                        player:set_sun({visible=true,sunrise_visible=true})
                        player:set_moon({visible=true})
                        player:set_stars({visible=true})
@@ -47,120 +60,343 @@ local update_player_sky = function()
 end
 
 --this tells the client mod to update the weather type
-local function_send_weather_type = function()
-       local channel = minetest.mod_channel_join("weather_type")
-       channel:send_all(tostring(weather_type))
-       channel:leave()
+function_send_weather_type = function()
+       weather_channel:send_all(tostring(weather_type))
 end
 
 --index all mods
 local all_nodes = {}
 minetest.register_on_mods_loaded(function()
-       for name in pairs(minetest.registered_nodes) do
+       for name in pairs(minetest.registered_items) do
                if name ~= "air" and name ~= "ignore" then
                        table.insert(all_nodes,name)
                end
-       end     
+       end
 end)
 
 --this sends the client all nodes that weather can be on top of
 --(everything)
+
+--have the client send the server the ready signal
+minetest.register_on_modchannel_message(function(channel_name, sender, message)
+       if channel_name == "weather_intake" then
+               minetest.after(0,function()
+               --print("sending player weather")
+               --for some reason this variable assignment does not work outside the scope of this function
+               local all_nodes_serialized = minetest.serialize(all_nodes)
+               weather_nodes_channel:send_all(all_nodes_serialized)
+               function_send_weather_type()
+               update_player_sky()
+               end)
+       end
+end)
+
+
 minetest.register_on_joinplayer(function(player)
-       minetest.after(5, function()
-               
-               local text = minetest.serialize(all_nodes)
-               local channel = minetest.mod_channel_join("weather_nodes")
-               channel:send_all(text)
-               channel:leave()
-               
+       minetest.after(3,function()
+               local all_nodes_serialized = minetest.serialize(all_nodes)
+               weather_nodes_channel:send_all(all_nodes_serialized)
                function_send_weather_type()
                update_player_sky()
        end)
 end)
 
-
 --spawn snow nodes
-local snow_timer = 0
-local do_snow = function(dtime)
-       snow_timer = snow_timer + dtime
-       if snow_timer > 3 then
-               snow_timer = 0
+local cDoSnow_call_count_for_blanket_coverage  = 50 -- how many calls of do_snow() are required for blanket snow coverage
+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)
+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)
+local cSnow_length_x = 80 -- (cSnow_length_x * cSnow_length_z) MUST be less than cSnowState_LFSR_length
+local cSnow_length_y = 80
+local cSnow_length_z = 80 -- (cSnow_length_x * cSnow_length_z) MUST be less than cSnowState_LFSR_length
+local snow_area = vector.new(cSnow_length_x, cSnow_length_y, cSnow_length_z)
+local snow_radius = vector.divide(snow_area, 2)
+local pos
+local min
+local max
+local subber = vector.subtract
+local adder  = vector.add
+local area_index
+local under_air = minetest.find_nodes_in_area_under_air
+local round_it = vector.round
+local n_vec = vector.new
+local lightlevel
+local get_light = minetest.get_node_light
+local g_node = minetest.get_node
+local node_name
+local def
+--local buildable
+local drawtype
+local walkable
+local liquid
+local r_nodes = minetest.registered_nodes
+local bulk_list
+local ice_list
+local spawn_table
+local mass_set = minetest.bulk_set_node
+local inserter = table.insert
+local temp_pos
+local floor, ceil = math.floor, math.ceil
+local weather_snowState
+local snowState_iterations_per_call  = ceil(cSnowState_LFSR_length / cDoSnow_call_count_for_blanket_coverage)
+local snowState_max_catchup_per_call = ceil(cSnowState_LFSR_length / cDoSnow_call_count_for_snowState_catchup)
+local under_air_iterations
+local catchup_steps
+local lsfr_steps_count
+local lsb
+local location_bits
+local relative_x
+local relative_z
+local under_air_count
+local x, y, z
+
+local acceptable_drawtypes = {
+       ["normal"] = true,
+       ["glasslike"] = true,
+       ["glasslike_framed"] = true,
+       ["glasslike_framed_optional"] = true,
+       ["allfaces"] = true,
+       ["allfaces_optional"] = true,
+}
+--this is debug
+--local average = {}
+
+function XOR( num1, num2 )
+       -- This XOR function is excerpted from the Bitwise Operations Mod v1.2, by Leslie E. Krause
+       -- which is provided under the MIT License (MIT)
+       --
+       -- The MIT License (MIT)
+       --
+       -- Copyright (c) 2020, Leslie Krause (leslie@searstower.org)
+       --
+       -- Permission is hereby granted, free of charge, to any person obtaining a copy of this
+       -- software and associated documentation files (the "Software"), to deal in the Software
+       -- without restriction, including without limitation the rights to use, copy, modify, merge,
+       -- publish, distribute, sublicense, and/or sell copies of the Software, and to permit
+       -- persons to whom the Software is furnished to do so, subject to the following conditions:
+       --
+       -- The above copyright notice and this permission notice shall be included in all copies or
+       -- substantial portions of the Software.
+       --
+       -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+       -- INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+       -- PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+       -- FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+       -- OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+       -- DEALINGS IN THE SOFTWARE.
+       --
+       -- For more details:
+       -- https://opensource.org/licenses/MIT
+
+       local exp = 1
+       local res = 0
+       while num1 > 0 or num2 > 0 do
+               local rem1 = num1 % 2
+               local rem2 = num2 % 2
+               if rem1 ~= rem2 then
+                       -- set each bit
+                       res = res + exp
+               end
+               num1 = ( num1 - rem1 ) / 2
+               num2 = ( num2 - rem2 ) / 2
+               exp = exp * 2
+       end
+       return res
+end
+
+
+local function do_snow()
+       if weather_type == 1 then
                for _,player in ipairs(minetest.get_connected_players()) do
-                       --print("running")
-                       local pos = player:get_pos()
-                       
-                       local meta = player:get_meta()
-                       local particle_table = {}
-                       
-                       local area = vector.new(40,40,40)
-                       
-                       local min = vector.subtract(pos, area)
-                       local max = vector.add(pos, area)
-                       
-                       
-                       local area_index = minetest.find_nodes_in_area_under_air(min, max, all_nodes)
-                       
-
-                       local spawn_table = {}
-                       for _,index in pairs(area_index) do
-                               if not spawn_table[index.x] then spawn_table[index.x] = {} end
-                               if not spawn_table[index.x][index.z] then
-                                       spawn_table[index.x][index.z] = index.y
-                               elseif spawn_table[index.x][index.z] < index.y then
-                                       spawn_table[index.x][index.z] = index.y
+                       --this is debug
+                       --local t0 = minetest.get_us_time()/1000000
+
+                       pos = round_it(player:get_pos())
+                       min = subber(pos, snow_radius)
+                       max = adder(pos, snow_radius)
+
+                       area_index = under_air(min, max, all_nodes)
+                       --local node_search_time = math.ceil((minetest.get_us_time()/1000000 - t0) * 1000)
+
+                       spawn_table = {}
+
+                       --the highest value is always indexed last in minetest.find_nodes_in_area_under_air,
+                       --so all that is needed is to iterate through it backwards and hook into the first
+                       --y value on the x and y and ignore the rest
+                       under_air_count = 0
+                       for key = #area_index,1,-1 do
+                               temp_pos = area_index[key]
+                               if not spawn_table[temp_pos.x] then spawn_table[temp_pos.x] = {} end
+                               if not spawn_table[temp_pos.x][temp_pos.z] then
+                                       spawn_table[temp_pos.x][temp_pos.z] = temp_pos.y
+                                       under_air_count = under_air_count + 1
                                end
                        end
-               
-                       
-                       --find the highest y value
-                       local bulk_list = {}
-                       for x,x_index in pairs(spawn_table) do
-                               for z,y in pairs(x_index) do
-                                       if math.random() > 0.995 then
-                                               local lightlevel = minetest.get_node_light(vector.new(x,y+1,z), 0.5)
-                                               if lightlevel >= 14 then
-                                                       --make it so buildable to nodes get replaced
-                                                       local node = minetest.get_node(vector.new(x,y,z)).name
-                                                       local def = minetest.registered_nodes[node]
-                                                       local buildable = def.buildable_to
-                                                       local walkable = def.walkable
-                                                       local liquid = (def.liquidtype ~= "none")
-                                                       
-                                                       if not liquid then
-                                                               if not buildable and minetest.get_node(vector.new(x,y+1,z)).name ~= "weather:snow" and walkable == true then
-                                                                       table.insert(bulk_list, vector.new(x,y+1,z))
-                                                               elseif buildable == true and node ~= "weather:snow" then
-                                                                       table.insert(bulk_list, vector.new(x,y,z))
+
+                       --save old method just in case useful or turns out it's faster after all
+                       --for _,index in pairs(area_index) do
+                       --      if not spawn_table[index.x] then spawn_table[index.x] = {} end
+                       --      if not spawn_table[index.x][index.z] then
+                       --              spawn_table[index.x][index.z] = index.y
+                       --      elseif spawn_table[index.x][index.z] < index.y then
+                       --              spawn_table[index.x][index.z] = index.y
+                       --      end
+                       --end
+
+                       bulk_list            = {}
+                       ice_list             = {}
+                       under_air_iterations = 0
+                       catchup_steps        = 0
+                       lsfr_steps_count     = 0
+                       repeat
+                               -- "fizzelfade" in the snow with a Linear Feedback Shift Register (LFSR)
+                               -- https://fabiensanglard.net/fizzlefade/index.php
+                               lsb = weather_snowState % 2 -- Get the output bit.
+                               weather_snowState = floor(weather_snowState / 2) -- Shift register
+                               if lsb == 1 then
+                                       weather_snowState = XOR(weather_snowState, cSnowState_LFSR_taps)
+                               end
+                               lsfr_steps_count = lsfr_steps_count + 1
+
+                               location_bits = weather_snowState - 1 -- LFSR values start at 1, but we want snow to be able to fall on (0, 0)
+                               relative_x = location_bits % cSnow_length_x
+                               relative_z = floor(location_bits / cSnow_length_x)
+
+                               if relative_z < cSnow_length_z then
+                                       x = (floor(min.x / cSnow_length_x) * cSnow_length_x) + relative_x -- align fizzelfade coords world-global
+                                       if x < min.x then x = x + cSnow_length_x end -- ensure it falls in the same space as area_index
+                                       local x_index = spawn_table[x]
+                                       if x_index ~= nil then
+                                               z = (floor(min.z / cSnow_length_z) * cSnow_length_z) + relative_z -- align fizzelfade coords world-global
+                                               if z < min.z then z = z + cSnow_length_z end -- ensure it falls in the same space as area_index
+                                               y = x_index[z]
+                                               if y ~= nil then
+
+                                                       -- We hit a location that's in the spawn_table
+                                                       under_air_iterations = under_air_iterations + 1
+
+                                                       lightlevel = get_light(n_vec(x,y+1,z), 0.5)
+                                                       if lightlevel >= 14 then
+                                                               -- daylight is above or near this node, so snow can fall on it
+
+                                                               node_name = g_node(n_vec(x,y,z)).name
+                                                               def = r_nodes[node_name]
+                                                               --buildable = def.buildable_to
+
+                                                               drawtype = acceptable_drawtypes[def.drawtype]
+
+                                                               walkable = def.walkable
+                                                               liquid = (def.liquidtype ~= "none")
+
+                                                               if not liquid and walkable and drawtype and node_name ~= "main:ice" then
+                                                                       --if buildable then
+                                                                       --      if node_name ~= "weather:snow" then
+                                                                       --              inserter(bulk_list, n_vec(x,y,z))
+                                                                       --      else
+                                                                       --              catchup_steps = catchup_steps + 1 -- we've already snowed on this spot
+                                                                       --      end
+                                                                       --elseif walkable then
+                                                                               if g_node(n_vec(x,y+1,z)).name ~= "weather:snow" then
+                                                                                       inserter(bulk_list, n_vec(x,y+1,z))
+                                                                               else
+                                                                                       catchup_steps = catchup_steps + 1 -- we've already snowed on this spot
+                                                                               end
+                                                                       --end
+                                                               elseif node_name == "main:water" then
+                                                                       inserter(ice_list, n_vec(x,y,z))
                                                                end
                                                        end
+
                                                end
                                        end
                                end
-                       end
+                       until (lsfr_steps_count - catchup_steps) >= snowState_iterations_per_call or catchup_steps >= snowState_max_catchup_per_call
+
                        if bulk_list then
-                               minetest.bulk_set_node(bulk_list, {name="weather:snow"})
+                               mass_set(bulk_list, {name="weather:snow"})
+                       end
+                       if ice_list then
+                               mass_set(ice_list, {name="main:ice"})
+                       end
+
+
+                       --this is debug
+                       --[[
+                       local chugent = math.ceil((minetest.get_us_time()/1000000 - t0) * 1000)
+                       print("---------------------------------")
+                       print("find_nodes_in_area_under_air() time: " .. node_search_time .. " ms")
+                       print("New Snow generation time:            " .. chugent .. " ms  [" .. (chugent - node_search_time) .. " ms]")
+
+                       inserter(average, chugent)
+                       local a = 0
+                       --don't cause memory leak
+                       if get_table_size(average) > 10 then
+                               table.remove(average,1)
                        end
+                       for _,i in ipairs(average) do
+                               a = a + i
+                       end
+                       print(dump(average))
+                       a = a / get_table_size(average)
+                       print("average = "..a.."ms")
+                       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)
+                       --print("---------------------------------")
+                       --]]--
                end
        end
+
+       minetest.after(3, function()
+               do_snow()
+       end)
 end
+minetest.register_on_mods_loaded(function()
+       minetest.after(0,function()
+               do_snow()
+       end)
+end)
 
 
 
 --this sets random weather
-local weather_timer_goal = (math.random(5,7)+math.random())*60
-minetest.register_globalstep(function(dtime)
-       weather_timer = weather_timer + dtime
-       if weather_timer >= weather_timer_goal then
-               weather_timer_goal = (math.random(5,7)+math.random())*60
-               weather_timer = 0
-               weather_type = math.random(0,weather_max)
-               function_send_weather_type()
-               update_player_sky()
+local initial_run = true
+local new_weather
+local function randomize_weather()
+       if not initial_run then
+               new_weather = math.random(0,weather_max)
+               if new_weather ~= weather_type or not weather_type then
+                       weather_type = new_weather
+               else
+                       weather_type = 0
+               end
+               mod_storage:set_int("weather_type", weather_type)
+       else
+               initial_run = false
        end
-       --spawn snow nodes
-       if weather_type == 1 then
-               do_snow(dtime)
+
+       function_send_weather_type()
+       update_player_sky()
+
+       minetest.after((math.random(15,20)+math.random())*60, function()
+               randomize_weather()
+       end)
+end
+
+minetest.register_on_mods_loaded(function()
+       minetest.after(0,function()
+       if mod_storage:get_int("weather_initialized") == 0 then
+               mod_storage:set_int("weather_initialized",1)
+               weather_type = math.random(0,weather_max)
+               mod_storage:set_int("weather_type", weather_type)
        end
+
+       weather_snowState = math.max(mod_storage:get_int("weather_snowState"), 1)
+
+       randomize_weather()
+       end)
+end)
+
+minetest.register_on_shutdown(function()
+       mod_storage:set_int("weather_type", weather_type)
+       mod_storage:set_int("weather_snowState", weather_snowState)
 end)
 
 local snowball_throw = function(player)
@@ -181,11 +417,13 @@ end
 
 minetest.register_node("weather:snow", {
     description = "Snow",
-    tiles = {"snow_block.png"},
-    groups = {soft = 1, shovel = 1, hand = 1,pathable = 1,wool=1,dirt=1,falling_node=1},
+    tiles = {"snow.png"},
+    groups = {pathable = 1,snow = 1, falling_node=1},
     sounds = main.woolSound(),
     paramtype = "light",
        drawtype = "nodebox",
+       walkable = false,
+       floodable = true,
     drop = {
                        max_items = 5,
                        items= {
@@ -216,6 +454,48 @@ minetest.register_node("weather:snow", {
        },
 })
 
+minetest.register_node("weather:snow_block", {
+    description = "Snow",
+    tiles = {"snow.png"},
+    groups = {pathable = 1,snow = 1},
+    sounds = main.woolSound(),
+    drop = {
+                       max_items = 5,
+                       items= {
+                               {
+                                       items = {"weather:snowball"},
+                               },
+                               {
+                                       items = {"weather:snowball"},
+                               },
+                               {
+                                       items = {"weather:snowball"},
+                               },
+                               {
+                                       items = {"weather:snowball"},
+                               },
+                               {
+                                       rarity = 5,
+                                       items = {"weather:snowball"},
+                               },
+                       },
+               },
+})
+
+minetest.register_abm({
+       label = "snow and ice melt",
+       nodenames = {"weather:snow","main:ice"},
+       neighbors = {"air"},
+       interval = 3,
+       chance = 10,
+       catch_up = true,
+       action = function(pos)
+               if weather_type ~= 1 then
+                       minetest.remove_node(pos)
+               end
+       end,
+})
+
 minetest.register_craftitem("weather:snowball", {
        description = "Snowball",
        inventory_image = "snowball.png",
@@ -265,60 +545,63 @@ snowball.on_step = function(self, dtime)
        local vel = self.object:get_velocity()
        local hit = false
        local pos = self.object:get_pos()
-       
+
        --hit object with the snowball
        for _,object in ipairs(minetest.get_objects_inside_radius(pos, 1)) do
-               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) then
-                       object:punch(self.object, 2, 
+               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
+                       object:punch(self.object, 2,
                                {
                                full_punch_interval=1.5,
-                               damage_groups = {fleshy=0},
+                               damage_groups = {damage=0,fleshy=0},
                        })
                        hit = true
                        break
                end
        end
-       
+
        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
-       
-               minetest.sound_play("wool",{pos=pos, pitch = math.random(80,100)/100})
-               minetest.add_particlespawner({
-                       amount = 20,
-                       -- Number of particles spawned over the time period `time`.
-
-                       time = 0.001,
-                       -- Lifespan of spawner in seconds.
-                       -- If time is 0 spawner has infinite lifespan and spawns the `amount` on
-                       -- a per-second basis.
-
-                       minpos = pos,
-                       maxpos = pos,
-                       minvel = {x=-2, y=3, z=-2},
-                       maxvel = {x=2, y=5, z=2},
-                       minacc = {x=0, y=-9.81, z=0},
-                       maxacc = {x=0, y=-9.81, z=0},
-                       minexptime = 1,
-                       maxexptime = 3,
-                       minsize = 1,
-                       maxsize = 1,
-                       -- The particles' properties are random values between the min and max
-                       -- values.
-                       -- pos, velocity, acceleration, expirationtime, size
-
-                       collisiondetection = true,
-
-                       collision_removal = true,
-
-                       object_collision = false,
-
-                       texture = "snowflake_"..math.random(1,2)..".png",
-
-               })
-               
-               self.object:remove()
-               
+               --snowballs explode in the nether
+               if pos.y <= -10033 and pos.y >= -20000 then
+                       self.object:remove()
+                       tnt(pos,4)
+               else
+                       minetest.sound_play("wool",{pos=pos, pitch = math.random(80,100)/100})
+                       minetest.add_particlespawner({
+                               amount = 20,
+                               -- Number of particles spawned over the time period `time`.
+
+                               time = 0.001,
+                               -- Lifespan of spawner in seconds.
+                               -- If time is 0 spawner has infinite lifespan and spawns the `amount` on
+                               -- a per-second basis.
+
+                               minpos = pos,
+                               maxpos = pos,
+                               minvel = {x=-2, y=3, z=-2},
+                               maxvel = {x=2, y=5, z=2},
+                               minacc = {x=0, y=-9.81, z=0},
+                               maxacc = {x=0, y=-9.81, z=0},
+                               minexptime = 1,
+                               maxexptime = 3,
+                               minsize = 1,
+                               maxsize = 1,
+                               -- The particles' properties are random values between the min and max
+                               -- values.
+                               -- pos, velocity, acceleration, expirationtime, size
+
+                               collisiondetection = true,
+
+                               collision_removal = true,
+
+                               object_collision = false,
+
+                               texture = "snowflake_"..math.random(1,2)..".png",
+
+                       })
+                       self.object:remove()
+               end
        end
-       
+
        self.oldvel = vel
 end
 minetest.register_entity("weather:snowball", snowball)