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