]> git.lizzy.rs Git - Crafter.git/blob - mods/weather/init.lua
24a8ad82cf262610414d4585ac1c74e2fe7f4efe
[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_items) 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 drawtype
129 local walkable
130 local liquid
131 local r_nodes = minetest.registered_nodes
132 local bulk_list
133 local ice_list
134 local spawn_table
135 local mass_set = minetest.bulk_set_node
136 local inserter = table.insert
137 local temp_pos
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
143 local catchup_steps
144 local lsfr_steps_count
145 local lsb
146 local location_bits
147 local relative_x
148 local relative_z
149 local under_air_count
150 local x, y, z
151
152 local acceptable_drawtypes = {
153         ["normal"] = true,
154         ["glasslike"] = true,
155         ["glasslike_framed"] = true,
156         ["glasslike_framed_optional"] = true,
157         ["allfaces"] = true,
158         ["allfaces_optional"] = true,
159 }
160 --this is debug
161 --local average = {}
162
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)
166         --
167         -- The MIT License (MIT)
168         --
169         -- Copyright (c) 2020, Leslie Krause (leslie@searstower.org)
170         --
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:
176         --
177         -- The above copyright notice and this permission notice shall be included in all copies or
178         -- substantial portions of the Software.
179         --
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.
186         --
187         -- For more details:
188         -- https://opensource.org/licenses/MIT
189
190         local exp = 1
191         local res = 0
192         while num1 > 0 or num2 > 0 do
193                 local rem1 = num1 % 2
194                 local rem2 = num2 % 2
195                 if rem1 ~= rem2 then
196                         -- set each bit
197                         res = res + exp
198                 end
199                 num1 = ( num1 - rem1 ) / 2
200                 num2 = ( num2 - rem2 ) / 2
201                 exp = exp * 2
202         end
203         return res
204 end
205
206
207 local function do_snow()
208         if weather_type == 1 then
209                 for _,player in ipairs(minetest.get_connected_players()) do
210                         --this is debug
211                         --local t0 = minetest.get_us_time()/1000000
212
213                         pos = round_it(player:get_pos())
214                         min = subber(pos, snow_radius)
215                         max = adder(pos, snow_radius)
216
217                         area_index = under_air(min, max, all_nodes)
218                         --local node_search_time = math.ceil((minetest.get_us_time()/1000000 - t0) * 1000)
219
220                         spawn_table = {}
221
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
225                         under_air_count = 0
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
232                                 end
233                         end
234
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
242                         --      end
243                         --end
244
245                         bulk_list            = {}
246                         ice_list             = {}
247                         under_air_iterations = 0
248                         catchup_steps        = 0
249                         lsfr_steps_count     = 0
250                         repeat
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
255                                 if lsb == 1 then
256                                         weather_snowState = XOR(weather_snowState, cSnowState_LFSR_taps)
257                                 end
258                                 lsfr_steps_count = lsfr_steps_count + 1
259
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)
263
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
271                                                 y = x_index[z]
272                                                 if y ~= nil then
273
274                                                         -- We hit a location that's in the spawn_table
275                                                         under_air_iterations = under_air_iterations + 1
276
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
280
281                                                                 node_name = g_node(n_vec(x,y,z)).name
282                                                                 def = r_nodes[node_name]
283                                                                 --buildable = def.buildable_to
284
285                                                                 drawtype = acceptable_drawtypes[def.drawtype]
286
287                                                                 walkable = def.walkable
288                                                                 liquid = (def.liquidtype ~= "none")
289
290                                                                 if not liquid and walkable and drawtype and node_name ~= "main:ice" then
291                                                                         --if buildable then
292                                                                         --      if node_name ~= "weather:snow" then
293                                                                         --              inserter(bulk_list, n_vec(x,y,z))
294                                                                         --      else
295                                                                         --              catchup_steps = catchup_steps + 1 -- we've already snowed on this spot
296                                                                         --      end
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))
300                                                                                 else
301                                                                                         catchup_steps = catchup_steps + 1 -- we've already snowed on this spot
302                                                                                 end
303                                                                         --end
304                                                                 elseif node_name == "main:water" then
305                                                                         inserter(ice_list, n_vec(x,y,z))
306                                                                 end
307                                                         end
308
309                                                 end
310                                         end
311                                 end
312                         until (lsfr_steps_count - catchup_steps) >= snowState_iterations_per_call or catchup_steps >= snowState_max_catchup_per_call
313
314                         if bulk_list then
315                                 mass_set(bulk_list, {name="weather:snow"})
316                         end
317                         if ice_list then
318                                 mass_set(ice_list, {name="main:ice"})
319                         end
320
321
322                         --this is debug
323                         --[[
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]")
328
329                         inserter(average, chugent)
330                         local a = 0
331                         --don't cause memory leak
332                         if get_table_size(average) > 10 then
333                                 table.remove(average,1)
334                         end
335                         for _,i in ipairs(average) do
336                                 a = a + i
337                         end
338                         print(dump(average))
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("---------------------------------")
343                         --]]--
344                 end
345         end
346
347         minetest.after(3, function()
348                 do_snow()
349         end)
350 end
351 minetest.register_on_mods_loaded(function()
352         minetest.after(0,function()
353                 do_snow()
354         end)
355 end)
356
357
358
359 --this sets random weather
360 local initial_run = true
361 local new_weather
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
367                 else
368                         weather_type = 0
369                 end
370                 mod_storage:set_int("weather_type", weather_type)
371         else
372                 initial_run = false
373         end
374
375         function_send_weather_type()
376         update_player_sky()
377
378         minetest.after((math.random(15,20)+math.random())*60, function()
379                 randomize_weather()
380         end)
381 end
382
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)
389         end
390
391         weather_snowState = math.max(mod_storage:get_int("weather_snowState"), 1)
392
393         randomize_weather()
394         end)
395 end)
396
397 minetest.register_on_shutdown(function()
398         mod_storage:set_int("weather_type", weather_type)
399         mod_storage:set_int("weather_snowState", weather_snowState)
400 end)
401
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")
409         if snowball then
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()
413                 return(true)
414         end
415         return(false)
416 end
417
418 minetest.register_node("weather:snow", {
419     description = "Snow",
420     tiles = {"snow_block.png"},
421     groups = {pathable = 1,snow = 1, falling_node=1},
422     sounds = main.woolSound(),
423     paramtype = "light",
424         drawtype = "nodebox",
425         walkable = false,
426         floodable = true,
427     drop = {
428                         max_items = 5,
429                         items= {
430                                 {
431                                         items = {"weather:snowball"},
432                                 },
433                                 {
434                                         items = {"weather:snowball"},
435                                 },
436                                 {
437                                         items = {"weather:snowball"},
438                                 },
439                                 {
440                                         items = {"weather:snowball"},
441                                 },
442                                 {
443                                         rarity = 5,
444                                         items = {"weather:snowball"},
445                                 },
446                         },
447                 },
448     buildable_to = true,
449     node_box = {
450                 type = "fixed",
451                 fixed = {
452                 {-8/16, -8/16, -8/16, 8/16, -6/16, 8/16},
453                 }
454         },
455 })
456
457 minetest.register_node("weather:snow_block", {
458     description = "Snow",
459     tiles = {"snow_block.png"},
460     groups = {pathable = 1,snow = 1},
461     sounds = main.woolSound(),
462     drop = {
463                         max_items = 5,
464                         items= {
465                                 {
466                                         items = {"weather:snowball"},
467                                 },
468                                 {
469                                         items = {"weather:snowball"},
470                                 },
471                                 {
472                                         items = {"weather:snowball"},
473                                 },
474                                 {
475                                         items = {"weather:snowball"},
476                                 },
477                                 {
478                                         rarity = 5,
479                                         items = {"weather:snowball"},
480                                 },
481                         },
482                 },
483 })
484
485 minetest.register_abm({
486         label = "snow and ice melt",
487         nodenames = {"weather:snow","main:ice"},
488         neighbors = {"air"},
489         interval = 3,
490         chance = 10,
491         catch_up = true,
492         action = function(pos)
493                 if weather_type ~= 1 then
494                         minetest.remove_node(pos)
495                 end
496         end,
497 })
498
499 minetest.register_craftitem("weather:snowball", {
500         description = "Snowball",
501         inventory_image = "snowball.png",
502         --stack_max = 1,
503         --range = 0,
504         on_place = function(itemstack, placer, pointed_thing)
505                 local worked = snowball_throw(placer)
506                 if worked then
507                         itemstack:take_item()
508                 end
509                 return(itemstack)
510         end,
511         on_secondary_use = function(itemstack, user, pointed_thing)
512                 local worked = snowball_throw(user)
513                 if worked then
514                         itemstack:take_item()
515                 end
516                 return(itemstack)
517         end,
518 })
519
520
521 snowball = {}
522 snowball.initial_properties = {
523         hp_max = 1,
524         physical = true,
525         collide_with_objects = false,
526         collisionbox = {-0.1, -0.1, -0.1, 0.1, 0.1, 0.1},
527         visual = "sprite",
528         visual_size = {x = 0.5, y = 0.5},
529         textures = {
530                 "snowball.png"
531         },
532         is_visible = true,
533         pointable = false,
534 }
535
536 snowball.snowball = true
537
538 snowball.on_activate = function(self)
539         self.object:set_acceleration(vector.new(0,-9.81,0))
540 end
541
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()
546         local hit = false
547         local pos = self.object:get_pos()
548
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,
553                                 {
554                                 full_punch_interval=1.5,
555                                 damage_groups = {damage=0,fleshy=0},
556                         })
557                         hit = true
558                         break
559                 end
560         end
561
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
565                         self.object:remove()
566                         tnt(pos,4)
567                 else
568                         minetest.sound_play("wool",{pos=pos, pitch = math.random(80,100)/100})
569                         minetest.add_particlespawner({
570                                 amount = 20,
571                                 -- Number of particles spawned over the time period `time`.
572
573                                 time = 0.001,
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.
577
578                                 minpos = pos,
579                                 maxpos = pos,
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},
584                                 minexptime = 1,
585                                 maxexptime = 3,
586                                 minsize = 1,
587                                 maxsize = 1,
588                                 -- The particles' properties are random values between the min and max
589                                 -- values.
590                                 -- pos, velocity, acceleration, expirationtime, size
591
592                                 collisiondetection = true,
593
594                                 collision_removal = true,
595
596                                 object_collision = false,
597
598                                 texture = "snowflake_"..math.random(1,2)..".png",
599
600                         })
601                         self.object:remove()
602                 end
603         end
604
605         self.oldvel = vel
606 end
607 minetest.register_entity("weather:snowball", snowball)