Do not recover wagons from minetest's static storage
[advtrains.git] / advtrains / advtrains / wagons.lua
1 --atan2 counts angles clockwise, minetest does counterclockwise
2
3 minetest.register_privilege("train_place", {
4 description = "Player can place trains on tracks not owned by player",
5 give_to_singleplayer= false,
6 });
7 minetest.register_privilege("train_remove", {
8 description = "Player can remove trains not owned by player",
9 give_to_singleplayer= false,
10 });
11
12 local wagon={
13 collisionbox = {-0.5,-0.5,-0.5, 0.5,0.5,0.5},
14 --physical = true,
15 visual = "mesh",
16 mesh = "wagon.b3d",
17 visual_size = {x=3, y=3},
18 textures = {"black.png"},
19 is_wagon=true,
20 wagon_span=1,--how many index units of space does this wagon consume
21 has_inventory=false,
22 }
23
24
25
26 function wagon:on_rightclick(clicker)
27 if not self:ensure_init() then return end
28 if not clicker or not clicker:is_player() then
29 return
30 end
31 if clicker:get_player_control().aux1 then
32 --advtrains.dumppath(self:train().path)
33 --minetest.chat_send_all("at index "..(self:train().index or "nil"))
34 --advtrains.invert_train(self.train_id)
35 atprint(dump(self))
36 return
37 end
38 local no=self:get_seatno(clicker:get_player_name())
39 if no then
40 self:get_off(no)
41 else
42 self:show_get_on_form(clicker:get_player_name())
43 end
44 end
45
46 function wagon:train()
47 return advtrains.trains[self.train_id]
48 end
49
50 --[[about 'initalized':
51 when initialized is false, the entity hasn't got any data yet and should wait for these to be set before doing anything
52 when loading an existing object (with staticdata), it will be set
53 when instanciating a new object via add_entity, it is not set at the time on_activate is called.
54 then, wagon:initialize() will be called
55
56 wagon will save only uid in staticdata, no serialized table
57 ]]
58 function wagon:on_activate(sd_uid, dtime_s)
59 if sd_uid~="" then
60 --destroy when loaded from static block.
61 self.object:remove()
62 return
63 end
64 self.object:set_armor_groups({immortal=1})
65 self.entity_name=self.name
66 end
67
68 function wagon:get_staticdata()
69 if not self:ensure_init() then return end
70 atprint("[wagon "..((self.unique_id and self.unique_id~="" and self.unique_id) or "no-id").."]: saving to wagon_save")
71 --serialize inventory, if it has one
72 if self.has_inventory then
73 local inv=minetest.get_inventory({type="detached", name="advtrains_wgn_"..self.unique_id})
74 self.ser_inv=advtrains.serialize_inventory(inv)
75 end
76 --save to table before being unloaded
77 advtrains.wagon_save[self.unique_id]=advtrains.merge_tables(self)
78 advtrains.wagon_save[self.unique_id].entity_name=self.name
79 advtrains.wagon_save[self.unique_id].name=nil
80 advtrains.wagon_save[self.unique_id].object=nil
81 return self.unique_id
82 end
83 --returns: uid of wagon
84 function wagon:init_new_instance(train_id, properties)
85 self.unique_id=os.time()..os.clock()
86 self.train_id=train_id
87 for k,v in pairs(properties) do
88 if k~="name" and k~="object" then
89 self[k]=v
90 end
91 end
92 self:init_shared()
93 self.initialized=true
94 atprint("init_new_instance "..self.unique_id.." ("..self.train_id..")")
95 return self.unique_id
96 end
97 function wagon:init_from_wagon_save(uid)
98 if not advtrains.wagon_save[uid] then
99 self.object:remove()
100 return
101 end
102 self.unique_id=uid
103 for k,v in pairs(advtrains.wagon_save[uid]) do
104 if k~="name" and k~="object" then
105 self[k]=v
106 end
107 end
108 if not self.train_id or not self:train() then
109 self.object:remove()
110 return
111 end
112 self:init_shared()
113 self.initialized=true
114 minetest.after(1, function() self:reattach_all() end)
115 atprint("init_from_wagon_save "..self.unique_id.." ("..self.train_id..")")
116 advtrains.update_trainpart_properties(self.train_id)
117 end
118 function wagon:init_shared()
119 if self.has_inventory then
120 local uid_noptr=self.unique_id..""
121 --to be used later
122 local inv=minetest.create_detached_inventory("advtrains_wgn_"..self.unique_id, {
123 allow_move = function(inv, from_list, from_index, to_list, to_index, count, player)
124 return count
125 end,
126 allow_put = function(inv, listname, index, stack, player)
127 return stack:get_count()
128 end,
129 allow_take = function(inv, listname, index, stack, player)
130 return stack:get_count()
131 end
132 })
133 if self.ser_inv then
134 advtrains.deserialize_inventory(self.ser_inv, inv)
135 end
136 if self.inventory_list_sizes then
137 for lst, siz in pairs(self.inventory_list_sizes) do
138 inv:set_size(lst, siz)
139 end
140 end
141 end
142 if self.doors then
143 self.door_anim_timer=0
144 self.door_state=0
145 end
146 if self.custom_on_activate then
147 self:custom_on_activate(dtime_s)
148 end
149 end
150 function wagon:ensure_init()
151 if self.initialized then
152 if self.noninitticks then self.noninitticks=nil end
153 return true
154 end
155 if not self.noninitticks then self.noninitticks=0 end
156 self.noninitticks=self.noninitticks+1
157 if self.noninitticks>20 then
158 self.object:remove()
159 else
160 self.object:setvelocity({x=0,y=0,z=0})
161 end
162 return false
163 end
164
165 -- Remove the wagon
166 function wagon:on_punch(puncher, time_from_last_punch, tool_capabilities, direction)
167 if not self:ensure_init() then return end
168 if not puncher or not puncher:is_player() then
169 return
170 end
171 if self.owner and puncher:get_player_name()~=self.owner and (not minetest.check_player_privs(puncher, {train_remove = true })) then
172 minetest.chat_send_player(puncher:get_player_name(), attrans("This wagon is owned by @1, you can't destroy it.", self.owner));
173 return
174 end
175
176 if minetest.setting_getbool("creative_mode") then
177 if not self:destroy() then return end
178
179 local inv = puncher:get_inventory()
180 if not inv:contains_item("main", self.name) then
181 inv:add_item("main", self.name)
182 end
183 else
184 local pc=puncher:get_player_control()
185 if not pc.sneak then
186 minetest.chat_send_player(puncher:get_player_name(), attrans("Warning: If you destroy this wagon, you only get some steel back! If you are sure, shift-leftclick the wagon."))
187 return
188 end
189
190 if not self:destroy() then return end
191
192 local inv = puncher:get_inventory()
193 for _,item in ipairs(self.drops or {self.name}) do
194 inv:add_item("main", item)
195 end
196 end
197 end
198 function wagon:destroy()
199 --some rules:
200 -- you get only some items back
201 -- single left-click shows warning
202 -- shift leftclick destroys
203 -- not when a driver is inside
204
205 for _,_ in pairs(self.seatp) do
206 return
207 end
208
209 if self.custom_may_destroy then
210 if not self.custom_may_destroy(self, puncher, time_from_last_punch, tool_capabilities, direction) then
211 return
212 end
213 end
214 if self.custom_on_destroy then
215 self.custom_on_destroy(self, puncher, time_from_last_punch, tool_capabilities, direction)
216 end
217
218 atprint("[wagon "..((self.unique_id and self.unique_id~="" and self.unique_id) or "no-id").."]: destroying")
219
220 self.object:remove()
221
222 table.remove(self:train().trainparts, self.pos_in_trainparts)
223 advtrains.update_trainpart_properties(self.train_id)
224 advtrains.wagon_save[self.unique_id]=nil
225 if self.discouple then self.discouple.object:remove() end--will have no effect on unloaded objects
226 return true
227 end
228
229
230 function wagon:on_step(dtime)
231 if not self:ensure_init() then return end
232
233 local t=os.clock()
234 local pos = self.object:getpos()
235
236 if not pos then
237 atprint("["..self.unique_id.."][fatal] missing position (object:getpos() returned nil)")
238 return
239 end
240
241 self.entity_name=self.name
242
243 --is my train still here
244 if not self.train_id or not self:train() then
245 atprint("[wagon "..self.unique_id.."] missing train_id, destroying")
246 self.object:remove()
247 return
248 elseif not self.initialized then
249 self.initialized=true
250 end
251 if not self.seatp then
252 self.seatp={}
253 end
254
255 --custom on_step function
256 if self.custom_on_step then
257 self:custom_on_step(self, dtime)
258 end
259
260 --driver control
261 for seatno, seat in ipairs(self.seats) do
262 if seat.driving_ctrl_access then
263 local driver=self.seatp[seatno] and minetest.get_player_by_name(self.seatp[seatno])
264 local get_off_pressed=false
265 if driver and driver:get_player_control_bits()~=self.old_player_control_bits then
266 local pc=driver:get_player_control()
267
268 advtrains.on_control_change(pc, self:train(), self.wagon_flipped)
269 if pc.aux1 and pc.sneak then
270 get_off_pressed=true
271 end
272
273 self.old_player_control_bits=driver:get_player_control_bits()
274 end
275 if driver then
276 if get_off_pressed then
277 self:get_off(seatno)
278 else
279 advtrains.update_driver_hud(driver:get_player_name(), self:train(), self.wagon_flipped)
280 end
281 end
282 end
283 end
284
285 local gp=self:train()
286
287 --door animation
288 if self.doors then
289 if (self.door_anim_timer or 0)<=0 then
290 local fct=self.wagon_flipped and -1 or 1
291 local dstate = (gp.door_open or 0) * fct
292 if dstate ~= self.door_state then
293 local at
294 --meaning of the train.door_open field:
295 -- -1: left doors (rel. to train orientation)
296 -- 0: closed
297 -- 1: right doors
298 --this code produces the following behavior:
299 -- if changed from 0 to +-1, play open anim. if changed from +-1 to 0, play close.
300 -- if changed from +-1 to -+1, first close and set 0, then it will detect state change again and run open.
301 if self.door_state == 0 then
302 at=self.doors.open[dstate]
303 self.object:set_animation(at.frames, at.speed or 15, at.blend or 0, false)
304 self.door_state = dstate
305 else
306 at=self.doors.close[self.door_state or 1]--in case it has not been set yet
307 self.object:set_animation(at.frames, at.speed or 15, at.blend or 0, false)
308 self.door_state = 0
309 end
310 self.door_anim_timer = at.time
311 end
312 else
313 self.door_anim_timer = (self.door_anim_timer or 0) - dtime
314 end
315 end
316 --DisCouple
317 if self.pos_in_trainparts and self.pos_in_trainparts>1 then
318 if gp.velocity==0 then
319 if not self.discouple or not self.discouple.object:getyaw() then
320 local object=minetest.add_entity(pos, "advtrains:discouple")
321 if object then
322 local le=object:get_luaentity()
323 le.wagon=self
324 --box is hidden when attached, so unuseful.
325 --object:set_attach(self.object, "", {x=0, y=0, z=self.wagon_span*10}, {x=0, y=0, z=0})
326 self.discouple=le
327 else
328 atprint("Couldn't spawn DisCouple")
329 end
330 end
331 else
332 if self.discouple and self.discouple.object:getyaw() then
333 self.discouple.object:remove()
334 end
335 end
336 end
337 --for path to be available. if not, skip step
338 if not advtrains.get_or_create_path(self.train_id, gp) then
339 self.object:setvelocity({x=0, y=0, z=0})
340 return
341 end
342 if not self.pos_in_train then
343 --why ever. but better continue next step...
344 advtrains.update_trainpart_properties(self.train_id)
345 return
346 end
347
348 local index=advtrains.get_real_path_index(self:train(), self.pos_in_train)
349 --atprint("trainindex "..gp.index.." wagonindex "..index)
350
351 --position recalculation
352 local first_pos=gp.path[math.floor(index)]
353 local second_pos=gp.path[math.floor(index)+1]
354 if not first_pos or not second_pos then
355 --atprint(" object "..self.unique_id.." path end reached!")
356 self.object:setvelocity({x=0,y=0,z=0})
357 return
358 end
359
360 --checking for environment collisions(a 3x3 cube around the center)
361 if not gp.recently_collided_with_env then
362 local collides=false
363 for x=-1,1 do
364 for y=0,2 do
365 for z=-1,1 do
366 local node=minetest.get_node_or_nil(vector.add(first_pos, {x=x, y=y, z=z}))
367 if (advtrains.train_collides(node)) then
368 collides=true
369 end
370 end
371 end
372 end
373 if collides then
374 if self.collision_count and self.collision_count>10 then
375 --enable collision mercy to get trains stuck in walls out of walls
376 --actually do nothing except limiting the velocity to 1
377 gp.velocity=math.min(gp.velocity, 1)
378 gp.tarvelocity=math.min(gp.tarvelocity, 1)
379 else
380 gp.recently_collided_with_env=true
381 gp.velocity=2*gp.velocity
382 gp.movedir=-gp.movedir
383 gp.tarvelocity=0
384 self.collision_count=(self.collision_count or 0)+1
385 end
386 else
387 self.collision_count=nil
388 end
389 end
390
391 --FIX: use index of the wagon, not of the train.
392 local velocity=(gp.velocity*gp.movedir)/(gp.path_dist[math.floor(index)] or 1)
393 local acceleration=(gp.last_accel or 0)/(gp.path_dist[math.floor(index)] or 1)
394 local factor=index-math.floor(index)
395 local actual_pos={x=first_pos.x-(first_pos.x-second_pos.x)*factor, y=first_pos.y-(first_pos.y-second_pos.y)*factor, z=first_pos.z-(first_pos.z-second_pos.z)*factor,}
396 local velocityvec={x=(first_pos.x-second_pos.x)*velocity*-1, z=(first_pos.z-second_pos.z)*velocity*-1, y=(first_pos.y-second_pos.y)*velocity*-1}
397 local accelerationvec={x=(first_pos.x-second_pos.x)*acceleration*-1, z=(first_pos.z-second_pos.z)*acceleration*-1, y=(first_pos.y-second_pos.y)*acceleration*-1}
398
399 --some additional positions to determine orientation
400 local aposfwd=gp.path[math.floor(index+2)]
401 local aposbwd=gp.path[math.floor(index-1)]
402
403 local yaw
404 if aposfwd and aposbwd then
405 yaw=advtrains.get_wagon_yaw(aposfwd, second_pos, first_pos, aposbwd, factor)+math.pi--TODO remove when cleaning up
406 else
407 yaw=math.atan2((first_pos.x-second_pos.x), (second_pos.z-first_pos.z))
408 end
409 if self.wagon_flipped then
410 yaw=yaw+math.pi
411 end
412
413 self.updatepct_timer=(self.updatepct_timer or 0)-dtime
414 if not self.old_velocity_vector
415 or not vector.equals(velocityvec, self.old_velocity_vector)
416 or not self.old_acceleration_vector
417 or not vector.equals(accelerationvec, self.old_acceleration_vector)
418 or self.old_yaw~=yaw
419 or self.updatepct_timer<=0 then--only send update packet if something changed
420 self.object:setpos(actual_pos)
421 self.object:setvelocity(velocityvec)
422 self.object:setacceleration(accelerationvec)
423 self.object:setyaw(yaw)
424 self.updatepct_timer=2
425 if self.update_animation then
426 self:update_animation(gp.velocity)
427 end
428 end
429
430
431 self.old_velocity_vector=velocityvec
432 self.old_acceleration_vector=accelerationvec
433 self.old_yaw=yaw
434 atprintbm("wagon step", t)
435 end
436
437 function advtrains.get_real_path_index(train, pit)
438 local pos_in_train_left=pit
439 local index=train.index
440 if pos_in_train_left>(index-math.floor(index))*(train.path_dist[math.floor(index)] or 1) then
441 pos_in_train_left=pos_in_train_left - (index-math.floor(index))*(train.path_dist[math.floor(index)] or 1)
442 index=math.floor(index)
443 while pos_in_train_left>(train.path_dist[index-1] or 1) do
444 pos_in_train_left=pos_in_train_left - (train.path_dist[index-1] or 1)
445 index=index-1
446 end
447 index=index-(pos_in_train_left/(train.path_dist[index-1] or 1))
448 else
449 index=index-(pos_in_train_left/(train.path_dist[math.floor(index-1)] or 1))
450 end
451 return index
452 end
453
454 function wagon:get_on(clicker, seatno)
455 if not self.seatp then
456 self.seatp={}
457 end
458 if not self.seats[seatno] then return end
459 if self.seatp[seatno] and self.seatp[seatno]~=clicker:get_player_name() then
460 self:get_off(seatno)
461 end
462 self.seatp[seatno] = clicker:get_player_name()
463 advtrains.player_to_train_mapping[clicker:get_player_name()]=self.train_id
464 clicker:set_attach(self.object, "", self.seats[seatno].attach_offset, {x=0,y=0,z=0})
465 clicker:set_eye_offset(self.seats[seatno].view_offset, self.seats[seatno].view_offset)
466 end
467 function wagon:get_off_plr(pname)
468 local no=self:get_seatno(pname)
469 if no then
470 self:get_off(no)
471 end
472 end
473 function wagon:get_seatno(pname)
474 for no, cont in pairs(self.seatp) do
475 if cont==pname then
476 return no
477 end
478 end
479 return nil
480 end
481 function wagon:get_off(seatno)
482 if not self.seatp[seatno] then return end
483 local pname = self.seatp[seatno]
484 local clicker = minetest.get_player_by_name(pname)
485 advtrains.player_to_train_mapping[pname]=nil
486 advtrains.clear_driver_hud(pname)
487 if clicker then
488 clicker:set_detach()
489 clicker:set_eye_offset({x=0,y=0,z=0}, {x=0,y=0,z=0})
490 local objpos=advtrains.round_vector_floor_y(self.object:getpos())
491 local yaw=self.object:getyaw()
492 local isx=(yaw < math.pi/4) or (yaw > 3*math.pi/4 and yaw < 5*math.pi/4) or (yaw > 7*math.pi/4)
493 --abuse helper function
494 for _,r in ipairs({-1, 1}) do
495 local p=vector.add({x=isx and r or 0, y=0, z=not isx and r or 0}, objpos)
496 if minetest.get_item_group(minetest.get_node(p).name, "platform")>0 then
497 minetest.after(0.2, function() clicker:setpos({x=p.x, y=p.y+1, z=p.z}) end)
498 end
499 end
500 end
501 self.seatp[seatno]=nil
502 end
503 function wagon:show_get_on_form(pname)
504 if not self.initialized then return end
505 if #self.seats==0 then
506 if self.has_inventory and self.get_inventory_formspec then
507 minetest.show_formspec(pname, "advtrains_inv_"..self.unique_id, self:get_inventory_formspec(pname))
508 end
509 return
510 end
511 local form, comma="size[5,8]label[0.5,0.5;"..attrans("Select seat:").."]textlist[0.5,1;4,6;seat;", ""
512 for seatno, seattbl in ipairs(self.seats) do
513 local addtext, colorcode="", ""
514 if self.seatp and self.seatp[seatno] then
515 colorcode="#FF0000"
516 addtext=" ("..self.seatp[seatno]..")"
517 end
518 form=form..comma..colorcode..seattbl.name..addtext
519 comma=","
520 end
521 form=form..";0,false]"
522 if self.has_inventory and self.get_inventory_formspec then
523 form=form.."button_exit[1,7;3,1;inv;"..attrans("Show Inventory").."]"
524 end
525 minetest.show_formspec(pname, "advtrains_geton_"..self.unique_id, form)
526 end
527 minetest.register_on_player_receive_fields(function(player, formname, fields)
528 local uid=string.match(formname, "^advtrains_geton_(.+)$")
529 if uid then
530 for _,wagon in pairs(minetest.luaentities) do
531 if wagon.is_wagon and wagon.initialized and wagon.unique_id==uid then
532 if fields.inv then
533 if wagon.has_inventory and wagon.get_inventory_formspec then
534 minetest.show_formspec(player:get_player_name(), "advtrains_inv_"..uid, wagon:get_inventory_formspec(player:get_player_name()))
535 end
536 elseif fields.seat then
537 local val=minetest.explode_textlist_event(fields.seat)
538 if val and val.type~="INV" and not wagon.seatp[player:get_player_name()] then
539 --get on
540 wagon:get_on(player, val.index)
541 --will work with the new close_formspec functionality. close exactly this formspec.
542 minetest.show_formspec(player:get_player_name(), formname, "")
543 end
544 end
545 end
546 end
547 end
548 end)
549 function wagon:reattach_all()
550 if not self.seatp then self.seatp={} end
551 for seatno, pname in pairs(self.seatp) do
552 local p=minetest.get_player_by_name(pname)
553 if p then
554 self:get_on(p ,seatno)
555 end
556 end
557 end
558 minetest.register_on_joinplayer(function(player)
559 for _,wagon in pairs(minetest.luaentities) do
560 if wagon.is_wagon and wagon.initialized then
561 wagon:reattach_all()
562 end
563 end
564 end)
565
566 function advtrains.register_wagon(sysname, prototype, desc, inv_img)
567 setmetatable(prototype, {__index=wagon})
568 minetest.register_entity(":advtrains:"..sysname,prototype)
569
570 minetest.register_craftitem(":advtrains:"..sysname, {
571 description = desc,
572 inventory_image = inv_img,
573 wield_image = inv_img,
574 stack_max = 1,
575
576 on_place = function(itemstack, placer, pointed_thing)
577 if not pointed_thing.type == "node" then
578 return
579 end
580
581
582 local node=minetest.get_node_or_nil(pointed_thing.under)
583 if not node then atprint("[advtrains]Ignore at placer position") return itemstack end
584 local nodename=node.name
585 if(not advtrains.is_track_and_drives_on(nodename, prototype.drives_on)) then
586 atprint("no track here, not placing.")
587 return itemstack
588 end
589 local conn1=advtrains.get_track_connections(node.name, node.param2)
590 local id=advtrains.create_new_train_at(pointed_thing.under, advtrains.dirCoordSet(pointed_thing.under, conn1))
591
592 local ob=minetest.add_entity(pointed_thing.under, "advtrains:"..sysname)
593 if not ob then
594 atprint("couldn't add_entity, aborting")
595 end
596 local le=ob:get_luaentity()
597
598 le.owner=placer:get_player_name()
599 le.infotext=desc..", owned by "..placer:get_player_name()
600
601 local wagon_uid=le:init_new_instance(id, {})
602
603 advtrains.add_wagon_to_train(le, id)
604 if not minetest.setting_getbool("creative_mode") then
605 itemstack:take_item()
606 end
607 return itemstack
608
609 end,
610 })
611 end
612
613 --[[
614 wagons can define update_animation(self, velocity) if they have a speed-dependent animation
615 this function will be called when the velocity vector changes or every 2 seconds.
616 ]]
617
618