]> git.lizzy.rs Git - Crafter.git/blob - mods/weather/init.lua
6020407531972779d38d166ceea74b12315c8e60
[Crafter.git] / mods / weather / init.lua
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")
5
6
7 weather_channel:send_all("")
8 weather_intake:send_all("")
9 weather_nodes_channel:send_all("")
10
11 local weather_max = 2
12 local mod_storage = minetest.get_mod_storage()
13
14 weather_type = mod_storage:get_int("weather_type")
15
16 local path = minetest.get_modpath(minetest.get_current_modname())
17 dofile(path.."/commands.lua")
18
19
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
24                         player:set_sky({
25                                 base_color="#808080",
26                                 type="plain",
27                                 clouds=false,
28
29                                 day_sky = "#808080",
30                                 dawn_horizon = "#808080",
31                                 dawn_sky = "#808080",
32                                 fog_sun_tint = "#808080",
33
34                                 night_sky="#808080",
35                                 night_horizon="#808080"
36                         })
37                         player:set_sun({visible=false,sunrise_visible=false})
38                         player:set_moon({visible=false})
39                         player:set_stars({visible=false})
40                 else
41                         player:set_sky({
42                                 base_color="#8cbafa",
43                                 type="regular",
44                                 clouds=true,
45
46                                 day_sky = "#8cbafa",
47
48                                 dawn_horizon = "#bac1f0",
49                                 dawn_sky = "#b4bafa",
50
51                                 night_sky="#006aff",
52                                 night_horizon="#4090ff"
53                         })
54
55                         player:set_sun({visible=true,sunrise_visible=true})
56                         player:set_moon({visible=true})
57                         player:set_stars({visible=true})
58                 end
59         end
60 end
61
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))
65 end
66
67 --index all mods
68 local all_nodes = {}
69 minetest.register_on_mods_loaded(function()
70         for name in pairs(minetest.registered_nodes) do
71                 if name ~= "air" and name ~= "ignore" then
72                         table.insert(all_nodes,name)
73                 end
74         end
75 end)
76
77 --this sends the client all nodes that weather can be on top of
78 --(everything)
79
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()
89                 update_player_sky()
90                 end)
91         end
92 end)
93
94
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()
100                 update_player_sky()
101         end)
102 end)
103
104 --spawn snow nodes
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)
113 local pos
114 local min
115 local max
116 local subber = vector.subtract
117 local adder  = vector.add
118 local area_index
119 local under_air = minetest.find_nodes_in_area_under_air
120 local round_it = vector.round
121 local n_vec = vector.new
122 local lightlevel
123 local get_light = minetest.get_node_light
124 local g_node = minetest.get_node
125 local node_name
126 local def
127 local buildable
128 local walkable
129 local liquid
130 local r_nodes = minetest.registered_nodes
131 local bulk_list
132 local ice_list
133 local spawn_table
134 local mass_set = minetest.bulk_set_node
135 local inserter = table.insert
136 local temp_pos
137 local floor, ceil = math.floor, math.ceil
138 local weather_snowState
139 local snowState_iterations_per_call  = ceil(cSnowState_LFSR_length / cDoSnow_call_count_for_blanket_coverage)
140 local snowState_max_catchup_per_call = ceil(cSnowState_LFSR_length / cDoSnow_call_count_for_snowState_catchup)
141 local under_air_iterations
142 local catchup_steps
143 local lsfr_steps_count
144 local lsb
145 local location_bits
146 local relative_x
147 local relative_z
148 local under_air_count
149 local x, y, z
150
151 --this is debug
152 --local average = {}
153
154 function XOR( num1, num2 )
155         -- This XOR function is excerpted from the Bitwise Operations Mod v1.2, by Leslie E. Krause
156         -- which is provided under the MIT License (MIT)
157         --
158         -- The MIT License (MIT)
159         --
160         -- Copyright (c) 2020, Leslie Krause (leslie@searstower.org)
161         --
162         -- Permission is hereby granted, free of charge, to any person obtaining a copy of this
163         -- software and associated documentation files (the "Software"), to deal in the Software
164         -- without restriction, including without limitation the rights to use, copy, modify, merge,
165         -- publish, distribute, sublicense, and/or sell copies of the Software, and to permit
166         -- persons to whom the Software is furnished to do so, subject to the following conditions:
167         --
168         -- The above copyright notice and this permission notice shall be included in all copies or
169         -- substantial portions of the Software.
170         --
171         -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
172         -- INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
173         -- PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
174         -- FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
175         -- OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
176         -- DEALINGS IN THE SOFTWARE.
177         --
178         -- For more details:
179         -- https://opensource.org/licenses/MIT
180
181         local exp = 1
182         local res = 0
183         while num1 > 0 or num2 > 0 do
184                 local rem1 = num1 % 2
185                 local rem2 = num2 % 2
186                 if rem1 ~= rem2 then
187                         -- set each bit
188                         res = res + exp
189                 end
190                 num1 = ( num1 - rem1 ) / 2
191                 num2 = ( num2 - rem2 ) / 2
192                 exp = exp * 2
193         end
194         return res
195 end
196
197
198 local function do_snow()
199         if weather_type == 1 then
200                 for _,player in ipairs(minetest.get_connected_players()) do
201                         --this is debug
202                         --local t0 = os.clock()
203
204                         pos = round_it(player:get_pos())
205                         min = subber(pos, snow_radius)
206                         max = adder(pos, snow_radius)
207
208                         area_index = under_air(min, max, all_nodes)
209                         --local node_search_time = math.ceil((os.clock() - t0) * 1000)
210
211                         spawn_table = {}
212
213                         --the highest value is always indexed last in minetest.find_nodes_in_area_under_air,
214                         --so all that is needed is to iterate through it backwards and hook into the first
215                         --y value on the x and y and ignore the rest
216                         under_air_count = 0
217                         for key = #area_index,1,-1 do
218                                 temp_pos = area_index[key]
219                                 if not spawn_table[temp_pos.x] then spawn_table[temp_pos.x] = {} end
220                                 if not spawn_table[temp_pos.x][temp_pos.z] then
221                                         spawn_table[temp_pos.x][temp_pos.z] = temp_pos.y
222                                         under_air_count = under_air_count + 1
223                                 end
224                         end
225
226                         --save old method just in case useful or turns out it's faster after all
227                         --for _,index in pairs(area_index) do
228                         --      if not spawn_table[index.x] then spawn_table[index.x] = {} end
229                         --      if not spawn_table[index.x][index.z] then
230                         --              spawn_table[index.x][index.z] = index.y
231                         --      elseif spawn_table[index.x][index.z] < index.y then
232                         --              spawn_table[index.x][index.z] = index.y
233                         --      end
234                         --end
235
236                         bulk_list            = {}
237                         ice_list             = {}
238                         under_air_iterations = 0
239                         catchup_steps        = 0
240                         lsfr_steps_count     = 0
241                         repeat
242                                 -- "fizzelfade" in the snow with a Linear Feedback Shift Register (LFSR)
243                                 -- https://fabiensanglard.net/fizzlefade/index.php
244                                 lsb = weather_snowState % 2 -- Get the output bit.
245                                 weather_snowState = floor(weather_snowState / 2) -- Shift register
246                                 if lsb == 1 then
247                                         weather_snowState = XOR(weather_snowState, cSnowState_LFSR_taps)
248                                 end
249                                 lsfr_steps_count = lsfr_steps_count + 1
250
251                                 location_bits = weather_snowState - 1 -- LFSR values start at 1, but we want snow to be able to fall on (0, 0)
252                                 relative_x = location_bits % cSnow_length_x
253                                 relative_z = floor(location_bits / cSnow_length_x)
254
255                                 if relative_z < cSnow_length_z then
256                                         x = (floor(min.x / cSnow_length_x) * cSnow_length_x) + relative_x -- align fizzelfade coords world-global
257                                         if x < min.x then x = x + cSnow_length_x end -- ensure it falls in the same space as area_index
258                                         local x_index = spawn_table[x]
259                                         if x_index ~= nil then
260                                                 z = (floor(min.z / cSnow_length_z) * cSnow_length_z) + relative_z -- align fizzelfade coords world-global
261                                                 if z < min.z then z = z + cSnow_length_z end -- ensure it falls in the same space as area_index
262                                                 y = x_index[z]
263                                                 if y ~= nil then
264
265                                                         -- We hit a location that's in the spawn_table
266                                                         under_air_iterations = under_air_iterations + 1
267
268                                                         lightlevel = get_light(n_vec(x,y+1,z), 0.5)
269                                                         if lightlevel >= 14 then
270                                                                 -- daylight is above or near this node, so snow can fall on it
271
272                                                                 --make it so buildable to nodes get replaced
273                                                                 node_name = g_node(n_vec(x,y,z)).name
274                                                                 def = r_nodes[node_name]
275                                                                 buildable = def.buildable_to
276                                                                 walkable = def.walkable
277                                                                 liquid = (def.liquidtype ~= "none")
278
279                                                                 if not liquid then
280                                                                         if buildable then
281                                                                                 if node_name ~= "weather:snow" then
282                                                                                         inserter(bulk_list, n_vec(x,y,z))
283                                                                                 else
284                                                                                         catchup_steps = catchup_steps + 1 -- we've already snowed on this spot
285                                                                                 end
286                                                                         elseif walkable then
287                                                                                 if g_node(n_vec(x,y+1,z)).name ~= "weather:snow" then
288                                                                                         inserter(bulk_list, n_vec(x,y+1,z))
289                                                                                 else
290                                                                                         catchup_steps = catchup_steps + 1 -- we've already snowed on this spot
291                                                                                 end
292                                                                         end
293                                                                 elseif node_name == "main:water" then
294                                                                         inserter(ice_list, n_vec(x,y,z))
295                                                                 end
296                                                         end
297
298                                                 end
299                                         end
300                                 end
301                         until (lsfr_steps_count - catchup_steps) >= snowState_iterations_per_call or catchup_steps >= snowState_max_catchup_per_call
302
303                         if bulk_list then
304                                 mass_set(bulk_list, {name="weather:snow"})
305                         end
306                         if ice_list then
307                                 mass_set(ice_list, {name="main:ice"})
308                         end
309
310
311                         --this is debug
312                         --[[
313                         local chugent = math.ceil((os.clock() - t0) * 1000)
314                         print("---------------------------------")
315                         print("find_nodes_in_area_under_air() time: " .. node_search_time .. " ms")
316                         print("New Snow generation time:            " .. chugent .. " ms  [" .. (chugent - node_search_time) .. " ms]")
317
318                         inserter(average, chugent)
319                         local a = 0
320                         --don't cause memory leak
321                         if get_table_size(average) > 10 then
322                                 table.remove(average,1)
323                         end
324                         for _,i in ipairs(average) do
325                                 a = a + i
326                         end
327                         print(dump(average))
328                         a = a / get_table_size(average)
329                         print("average = "..a.."ms")
330                         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)
331                         --print("---------------------------------")
332                         --]]--
333                 end
334         end
335
336         minetest.after(3, function()
337                 do_snow()
338         end)
339 end
340 minetest.register_on_mods_loaded(function()
341         minetest.after(0,function()
342                 do_snow()
343         end)
344 end)
345
346
347
348 --this sets random weather
349 local initial_run = true
350 local function randomize_weather()
351         if not initial_run then
352                 weather_type = math.random(0,weather_max)
353                 mod_storage:set_int("weather_type", weather_type)
354         else
355                 initial_run = false
356         end
357
358         function_send_weather_type()
359         update_player_sky()
360
361         minetest.after((math.random(5,7)+math.random())*60, function()
362                 randomize_weather()
363         end)
364 end
365
366 minetest.register_on_mods_loaded(function()
367         minetest.after(0,function()
368         if mod_storage:get_int("weather_initialized") == 0 then
369                 mod_storage:set_int("weather_initialized",1)
370                 weather_type = math.random(0,weather_max)
371                 mod_storage:set_int("weather_type", weather_type)
372         end
373
374         weather_snowState = math.max(mod_storage:get_int("weather_snowState"), 1)
375
376         randomize_weather()
377         end)
378 end)
379
380 minetest.register_on_shutdown(function()
381         mod_storage:set_int("weather_type", weather_type)
382         mod_storage:set_int("weather_snowState", weather_snowState)
383 end)
384
385 local snowball_throw = function(player)
386         local pos = player:get_pos()
387         pos.y = pos.y + 1.625
388         --let other players hear the noise too
389         minetest.sound_play("woosh",{to_player=player:get_player_name(), pitch = math.random(80,100)/100})
390         minetest.sound_play("woosh",{pos=pos, exclude_player = player:get_player_name(), pitch = math.random(80,100)/100})
391         local snowball = minetest.add_entity(pos,"weather:snowball")
392         if snowball then
393                 local vel = player:get_player_velocity()
394                 snowball:set_velocity(vector.add(vel,vector.multiply(player:get_look_dir(),20)))
395                 snowball:get_luaentity().thrower = player:get_player_name()
396                 return(true)
397         end
398         return(false)
399 end
400
401 minetest.register_node("weather:snow", {
402     description = "Snow",
403     tiles = {"snow_block.png"},
404     groups = {pathable = 1,snow = 1, falling_node=1},
405     sounds = main.woolSound(),
406     paramtype = "light",
407         drawtype = "nodebox",
408         walkable = false,
409         floodable = true,
410     drop = {
411                         max_items = 5,
412                         items= {
413                                 {
414                                         items = {"weather:snowball"},
415                                 },
416                                 {
417                                         items = {"weather:snowball"},
418                                 },
419                                 {
420                                         items = {"weather:snowball"},
421                                 },
422                                 {
423                                         items = {"weather:snowball"},
424                                 },
425                                 {
426                                         rarity = 5,
427                                         items = {"weather:snowball"},
428                                 },
429                         },
430                 },
431     buildable_to = true,
432     node_box = {
433                 type = "fixed",
434                 fixed = {
435                 {-8/16, -8/16, -8/16, 8/16, -6/16, 8/16},
436                 }
437         },
438 })
439
440 minetest.register_node("weather:snow_block", {
441     description = "Snow",
442     tiles = {"snow_block.png"},
443     groups = {pathable = 1,snow = 1},
444     sounds = main.woolSound(),
445     drop = {
446                         max_items = 5,
447                         items= {
448                                 {
449                                         items = {"weather:snowball"},
450                                 },
451                                 {
452                                         items = {"weather:snowball"},
453                                 },
454                                 {
455                                         items = {"weather:snowball"},
456                                 },
457                                 {
458                                         items = {"weather:snowball"},
459                                 },
460                                 {
461                                         rarity = 5,
462                                         items = {"weather:snowball"},
463                                 },
464                         },
465                 },
466 })
467
468 minetest.register_abm({
469         label = "snow and ice melt",
470         nodenames = {"weather:snow","main:ice"},
471         neighbors = {"air"},
472         interval = 3,
473         chance = 10,
474         catch_up = true,
475         action = function(pos)
476                 if weather_type ~= 1 then
477                         minetest.remove_node(pos)
478                 end
479         end,
480 })
481
482 minetest.register_craftitem("weather:snowball", {
483         description = "Snowball",
484         inventory_image = "snowball.png",
485         --stack_max = 1,
486         --range = 0,
487         on_place = function(itemstack, placer, pointed_thing)
488                 local worked = snowball_throw(placer)
489                 if worked then
490                         itemstack:take_item()
491                 end
492                 return(itemstack)
493         end,
494         on_secondary_use = function(itemstack, user, pointed_thing)
495                 local worked = snowball_throw(user)
496                 if worked then
497                         itemstack:take_item()
498                 end
499                 return(itemstack)
500         end,
501 })
502
503
504 snowball = {}
505 snowball.initial_properties = {
506         hp_max = 1,
507         physical = true,
508         collide_with_objects = false,
509         collisionbox = {-0.1, -0.1, -0.1, 0.1, 0.1, 0.1},
510         visual = "sprite",
511         visual_size = {x = 0.5, y = 0.5},
512         textures = {
513                 "snowball.png"
514         },
515         is_visible = true,
516         pointable = false,
517 }
518
519 snowball.snowball = true
520
521 snowball.on_activate = function(self)
522         self.object:set_acceleration(vector.new(0,-9.81,0))
523 end
524
525 --make this as efficient as possible
526 --make it so you can hit one snowball with another
527 snowball.on_step = function(self, dtime)
528         local vel = self.object:get_velocity()
529         local hit = false
530         local pos = self.object:get_pos()
531
532         --hit object with the snowball
533         for _,object in ipairs(minetest.get_objects_inside_radius(pos, 1)) do
534                 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
535                         object:punch(self.object, 2,
536                                 {
537                                 full_punch_interval=1.5,
538                                 damage_groups = {damage=0,fleshy=0},
539                         })
540                         hit = true
541                         break
542                 end
543         end
544
545         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
546                 --snowballs explode in the nether
547                 if pos.y <= -10000 and pos.y >= -20000 then
548                         self.object:remove()
549                         tnt(pos,4)
550                 else
551                         minetest.sound_play("wool",{pos=pos, pitch = math.random(80,100)/100})
552                         minetest.add_particlespawner({
553                                 amount = 20,
554                                 -- Number of particles spawned over the time period `time`.
555
556                                 time = 0.001,
557                                 -- Lifespan of spawner in seconds.
558                                 -- If time is 0 spawner has infinite lifespan and spawns the `amount` on
559                                 -- a per-second basis.
560
561                                 minpos = pos,
562                                 maxpos = pos,
563                                 minvel = {x=-2, y=3, z=-2},
564                                 maxvel = {x=2, y=5, z=2},
565                                 minacc = {x=0, y=-9.81, z=0},
566                                 maxacc = {x=0, y=-9.81, z=0},
567                                 minexptime = 1,
568                                 maxexptime = 3,
569                                 minsize = 1,
570                                 maxsize = 1,
571                                 -- The particles' properties are random values between the min and max
572                                 -- values.
573                                 -- pos, velocity, acceleration, expirationtime, size
574
575                                 collisiondetection = true,
576
577                                 collision_removal = true,
578
579                                 object_collision = false,
580
581                                 texture = "snowflake_"..math.random(1,2)..".png",
582
583                         })
584                         self.object:remove()
585                 end
586         end
587
588         self.oldvel = vel
589 end
590 minetest.register_entity("weather:snowball", snowball)