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