--atan2 counts angles clockwise, minetest does counterclockwise
minetest.register_privilege("train_place", {
description ="Player can place trains on tracks not owned by player",
give_to_singleplayer=false,});
minetest.register_privilege("train_remove", {
description ="Player can remove trains not owned by player",
give_to_singleplayer=false,});
minetest.register_privilege("train_operator", {
description ="Player may operate trains and switch signals. Given by default. Revoke to prevent players from griefing automated subway systems.",
give_to_singleplayer=true,
default=true,});local wagon={
collisionbox = {-0.5,-0.5,-0.5,0.5,0.5,0.5},--physical = true,
visual ="mesh",
mesh ="wagon.b3d",
visual_size = {x=3, y=3},
textures = {"black.png"},
is_wagon=true,
wagon_span=1,--how many index units of space does this wagon consume
has_inventory=false,}function wagon:train()return advtrains.trains[self.train_id]end--[[about 'initalized': when initialized is false, the entity hasn't got any data yet and should wait for these to be set before doing anything when loading an existing object (with staticdata), it will be set when instanciating a new object via add_entity, it is not set at the time on_activate is called. then, wagon:initialize() will be called wagon will save only uid in staticdata, no serialized table]]function wagon:on_activate(sd_uid, dtime_s)if sd_uid~=""then--destroy when loaded from static block.
self.object:remove()returnend
self.object:set_armor_groups({immortal=1})
self.entity_name=self.name
endfunction wagon:get_staticdata()return advtrains.pcall(function()if not self:ensure_init()then return endatprint("[wagon "..((self.unique_id and self.unique_id~=""and self.unique_id)or"no-id").."]: saving to wagon_save")--serialize inventory, if it has oneif self.has_inventory thenlocal inv=minetest.get_inventory({type="detached", name="advtrains_wgn_"..self.unique_id})
self.ser_inv=advtrains.serialize_inventory(inv)end--save to table before being unloaded
advtrains.wagon_save[self.unique_id]=advtrains.save_keys(self, {"seatp","owner","ser_inv","wagon_flipped","train_id","dcpl_lock"})
advtrains.wagon_save[self.unique_id].entity_name=self.name
return self.unique_id
end)end--returns: uid of wagonfunction wagon:init_new_instance(train_id, properties)local new_id=advtrains.random_id()while advtrains.wagon_save[new_id]do new_id=advtrains.random_id()end--ensure uniqueness
self.unique_id=new_id
self.train_id=train_id
for k,v inpairs(properties)doif k~="name"and k~="object"then
self[k]=v
endend
self:init_shared()
self.initialized=trueatprint("init_new_instance "..self.unique_id.." ("..self.train_id..")")return self.unique_id
endfunction wagon:init_from_wagon_save(uid)if not advtrains.wagon_save[uid]then
self.object:remove()returnend
self.unique_id=uid
for k,v inpairs(advtrains.wagon_save[uid])doif k~="name"and k~="object"then
self[k]=v
endendif not self.train_id or not self:train()then
self.object:remove()returnend
self:init_shared()
self.initialized=true
minetest.after(0.2,function() self:reattach_all()end)atprint("init_from_wagon_save "..self.unique_id.." ("..self.train_id..")")endfunction wagon:init_shared()if self.has_inventory thenlocal uid_noptr=self.unique_id..""--to be used laterlocal inv=minetest.create_detached_inventory("advtrains_wgn_"..self.unique_id, {
allow_move =function(inv, from_list, from_index, to_list, to_index, count, player)return count
end,
allow_put =function(inv, listname, index, stack, player)return stack:get_count()end,
allow_take =function(inv, listname, index, stack, player)return stack:get_count()end})if self.ser_inv then
advtrains.deserialize_inventory(self.ser_inv, inv)endif self.inventory_list_sizes thenfor lst, siz inpairs(self.inventory_list_sizes)do
inv:set_size(lst, siz)endendendif self.doors then
self.door_anim_timer=0
self.door_state=0endif self.custom_on_activate then
self:custom_on_activate(dtime_s)end-- reset line and infotext cache to update object properties on first call
self.line_cache=nil
self.infotext_cache=nilendfunction wagon:ensure_init()if self.initialized thenif self.noninitticks then self.noninitticks=nil endreturn trueendif not self.noninitticks then self.noninitticks=0end
self.noninitticks=self.noninitticks+1if self.noninitticks>20then
self.object:remove()else
self.object:setvelocity({x=0,y=0,z=0})endreturn falseend-- Remove the wagonfunction wagon:on_punch(puncher, time_from_last_punch, tool_capabilities, direction)return advtrains.pcall(function()if not self:ensure_init()then return endif not puncher or not puncher:is_player()thenreturnendif self.owner and puncher:get_player_name()~=self.owner and(not minetest.check_player_privs(puncher, {train_remove =true}))then
minetest.chat_send_player(puncher:get_player_name(),attrans("This wagon is owned by @1, you can't destroy it.", self.owner));returnendif#(self:train().trainparts)>1then
minetest.chat_send_player(puncher:get_player_name(),attrans("Wagon needs to be decoupled from other wagons in order to destroy it."));returnendlocal pc=puncher:get_player_control()if not pc.sneak then
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, hold Sneak and left-click the wagon."))returnendif not self:destroy()then return endlocal inv = puncher:get_inventory()for _,item inipairs(self.drops or{self.name})do
inv:add_item("main", item)endend)endfunction wagon:destroy()--some rules:-- you get only some items back-- single left-click shows warning-- shift leftclick destroys-- not when a driver is insidefor _,_ inpairs(self.seatp)doreturnendif self.custom_may_destroy thenif not self.custom_may_destroy(self, puncher, time_from_last_punch, tool_capabilities, direction)thenreturnendendif self.custom_on_destroy then
self.custom_on_destroy(self, puncher, time_from_last_punch, tool_capabilities, direction)endatprint("[wagon "..((self.unique_id and self.unique_id~=""and self.unique_id)or"no-id").."]: destroying")
self.object:remove()table.remove(self:train().trainparts, self.pos_in_trainparts)
advtrains.update_trainpart_properties(self.train_id)
advtrains.wagon_save[self.unique_id]=nilif self.discouple then self.discouple.object:remove()end--will have no effect on unloaded objectsreturn trueendfunction wagon:on_step(dtime)return advtrains.pcall(function()if not self:ensure_init()then return endlocal t=os.clock()local pos = self.object:getpos()if not pos thenatprint("["..self.unique_id.."][fatal] missing position (object:getpos() returned nil)")returnend
self.entity_name=self.name
--is my train still hereif not self.train_id or not self:train()thenatprint("[wagon "..self.unique_id.."] missing train_id, destroying")
self.object:remove()returnendif not self.seatp then
self.seatp={}endif not self.seatpc then
self.seatpc={}end--custom on_step functionif self.custom_on_step then
self:custom_on_step(self, dtime)end--driver controlfor seatno, seat inipairs(self.seats)dolocal driver=self.seatp[seatno]and minetest.get_player_by_name(self.seatp[seatno])local has_driverstand = self.seatp[seatno]and minetest.check_player_privs(self.seatp[seatno], {train_operator=true})if self.seat_groups then
has_driverstand = has_driverstand and(seat.driving_ctrl_access or self.seat_groups[seat.group].driving_ctrl_access)else
has_driverstand = has_driverstand and(seat.driving_ctrl_access)endif has_driverstand and driver then
advtrains.update_driver_hud(driver:get_player_name(), self:train(), self.wagon_flipped)elseif driver then--only show the inside textlocal inside=self:train().text_inside or""
advtrains.set_trainhud(driver:get_player_name(), inside)endif driver and driver:get_player_control_bits()~=self.seatpc[seatno]thenlocal pc=driver:get_player_control()
self.seatpc[seatno]=driver:get_player_control_bits()if has_driverstand then--regular driver stand controls
advtrains.on_control_change(pc, self:train(), self.wagon_flipped)--bordcomif pc.sneak and pc.jump then
self:show_bordcom(self.seatp[seatno])end--sound horn when requiredif self.horn_sound and pc.aux1 and not pc.sneak and not self.horn_handle then
self.horn_handle = minetest.sound_play(self.horn_sound, {
object = self.object,
gain =1.0,-- default
max_hear_distance =128,-- default, uses an euclidean metric
loop =true,})elseif not pc.aux1 and self.horn_handle then
minetest.sound_stop(self.horn_handle)
self.horn_handle =nilendelse-- If on a passenger seat and doors are open, get off when W or D pressed.local pass = self.seatp[seatno]and minetest.get_player_by_name(self.seatp[seatno])if pass and self:train().door_open~=0thenlocal pc=pass:get_player_control()if pc.up or pc.down then
self:get_off(seatno)endendendif pc.aux1 and pc.sneak then
self:get_off(seatno)endendend--check infotextlocal outside=self:train().text_outside or""local gp=self:train()--show off-track information in outside text instead of notifying the whole server about thislocal front_off_track=gp.max_index_on_track and gp.index and gp.index>gp.max_index_on_track
local back_off_track=gp.min_index_on_track and gp.end_index and gp.end_index<gp.min_index_on_track
if front_off_track or back_off_track then
outside = outside .."\n!!! Train off track !!!"endif self.infotext_cache~=outside then
self.object:set_properties({infotext=outside})
self.infotext_cache=outside
endlocal fct=self.wagon_flipped and-1or1--set line numberif self.name =="advtrains:subway_wagon"and gp.line and gp.line~=self.line_cache thenlocal new_line_tex="advtrains_subway_wagon.png^advtrains_subway_wagon_line"..gp.line..".png"
self.object:set_properties({
textures={new_line_tex},})
self.line_cache=gp.line
elseif self.line_cache~=nil and gp.line==nil then
self.object:set_properties({
textures=self.textures,})
self.line_cache=nilend--door animationif self.doors thenif(self.door_anim_timer or0)<=0thenlocal dstate = (gp.door_open or0) * fct
if dstate ~= self.door_state thenlocal at
--meaning of the train.door_open field:-- -1: left doors (rel. to train orientation)-- 0: closed-- 1: right doors--this code produces the following behavior:-- if changed from 0 to +-1, play open anim. if changed from +-1 to 0, play close.-- if changed from +-1 to -+1, first close and set 0, then it will detect state change again and run open.if self.door_state ==0thenif self.doors.open.sound then minetest.sound_play(self.doors.open.sound, {object = self.object})end
at=self.doors.open[dstate]
self.object:set_animation(at.frames, at.speed or15, at.blend or0,false)
self.door_state = dstate
elseif self.doors.close.sound then minetest.sound_play(self.doors.close.sound, {object = self.object})end
at=self.doors.close[self.door_state or1]--in case it has not been set yet
self.object:set_animation(at.frames, at.speed or15, at.blend or0,false)
self.door_state =0end
self.door_anim_timer = at.time
endelse
self.door_anim_timer = (self.door_anim_timer or0) - dtime
endend--DisCoupleif self.pos_in_trainparts and self.pos_in_trainparts>1thenif gp.velocity==0thenif not self.discouple or not self.discouple.object:getyaw()thenlocal object=minetest.add_entity(pos,"advtrains:discouple")if object thenlocal le=object:get_luaentity()
le.wagon=self
--box is hidden when attached, so unuseful.--object:set_attach(self.object, "", {x=0, y=0, z=self.wagon_span*10}, {x=0, y=0, z=0})
self.discouple=le
elseatprint("Couldn't spawn DisCouple")endendelseif self.discouple and self.discouple.object:getyaw()then
self.discouple.object:remove()endendend--for path to be available. if not, skip stepif not gp.path then
self.object:setvelocity({x=0, y=0, z=0})returnendif not self.pos_in_train then--why ever. but better continue next step...
advtrains.update_trainpart_properties(self.train_id)returnendlocal index=advtrains.get_real_path_index(self:train(), self.pos_in_train)--atprint("trainindex "..gp.index.." wagonindex "..index)--automatic get_on--needs to know index and pathif self.door_entry and gp.door_open and gp.door_open~=0and gp.velocity==0then--using the mapping created by the trainlogic globalstepfor i, ino inipairs(self.door_entry)do--fct is the flipstate flag from door animation abovelocal aci = index + ino*fct
local ix1=gp.path[math.floor(aci)]local ix2=gp.path[math.floor(aci+1)]-- the two wanted positions are ix1 and ix2 + (2nd-1st rotated by 90deg)-- (x z) rotated by 90deg is (-z x) (http://stackoverflow.com/a/4780141)local add = { x = (ix2.z-ix1.z)*gp.door_open, y =0, z = (ix1.x-ix2.x)*gp.door_open }local pts1=vector.round(vector.add(ix1, add))local pts2=vector.round(vector.add(ix2, add))if minetest.get_item_group(minetest.get_node(pts1).name,"platform")>0thenlocal ckpts={
pts1,
pts2,
vector.add(pts1, {x=0, y=1, z=0}),
vector.add(pts2, {x=0, y=1, z=0}),}for _,ckpos inipairs(ckpts)dolocal cpp=minetest.pos_to_string(ckpos)if advtrains.playersbypts[cpp]then
self:on_rightclick(advtrains.playersbypts[cpp])endendendendend--position recalculationlocal first_pos=gp.path[math.floor(index)]local second_pos=gp.path[math.floor(index)+1]if not first_pos or not second_pos then--atprint(" object "..self.unique_id.." path end reached!")
self.object:setvelocity({x=0,y=0,z=0})returnend--checking for environment collisions(a 3x3 cube around the center)if not gp.recently_collided_with_env thenlocal collides=falselocal exh = self.extent_h or1local exv = self.extent_v or2for x=-exh,exh dofor y=0,exv dofor z=-exh,exh dolocal node=minetest.get_node_or_nil(vector.add(first_pos, {x=x, y=y, z=z}))if(advtrains.train_collides(node))then
collides=trueendendendendif collides thenif self.collision_count and self.collision_count>10then--enable collision mercy to get trains stuck in walls out of walls--actually do nothing except limiting the velocity to 1
gp.velocity=math.min(gp.velocity,1)
gp.tarvelocity=math.min(gp.tarvelocity,1)else
gp.recently_collided_with_env=true
gp.velocity=2*gp.velocity
gp.movedir=-gp.movedir
gp.tarvelocity=0
self.collision_count=(self.collision_count or0)+1endelse
self.collision_count=nilendend--FIX: use index of the wagon, not of the train.local velocity=(gp.velocity*gp.movedir)/(gp.path_dist[math.floor(index)]or1)local acceleration=(gp.last_accel or0)/(gp.path_dist[math.floor(index)]or1)local factor=index-math.floor(index)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,}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}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}--some additional positions to determine orientationlocal aposfwd=gp.path[math.floor(index+2)]local aposbwd=gp.path[math.floor(index-1)]local yaw
if aposfwd and aposbwd then
yaw=advtrains.get_wagon_yaw(aposfwd, second_pos, first_pos, aposbwd, factor)+math.pi--TODO remove when cleaning upelse
yaw=math.atan2((first_pos.x-second_pos.x), (second_pos.z-first_pos.z))endif self.wagon_flipped then
yaw=yaw+math.pi
end
self.updatepct_timer=(self.updatepct_timer or0)-dtime
if not self.old_velocity_vector
or not vector.equals(velocityvec, self.old_velocity_vector)or not self.old_acceleration_vector
or not vector.equals(accelerationvec, self.old_acceleration_vector)or self.old_yaw~=yaw
or self.updatepct_timer<=0then--only send update packet if something changed
self.object:setpos(actual_pos)
self.object:setvelocity(velocityvec)
self.object:setacceleration(accelerationvec)if#self.seats >0and self.old_yaw ~= yaw thenif not self.player_yaw then
self.player_yaw = {}endif not self.old_yaw then
self.old_yaw=yaw
endfor _,name inpairs(self.seatp)dolocal p = minetest.get_player_by_name(name)if p thenif not self.turning then-- save player looking direction offset
self.player_yaw[name] = p:get_look_horizontal()-self.old_yaw
end-- set player looking direction using calculated offset
p:set_look_horizontal(self.player_yaw[name]+yaw)endend
self.turning =trueelseif self.old_yaw == yaw then-- train is no longer turning
self.turning =falseend
self.object:setyaw(yaw)
self.updatepct_timer=2if self.update_animation then
self:update_animation(gp.velocity, self.old_velocity)endif self.custom_on_velocity_change then
self:custom_on_velocity_change(gp.velocity, self.old_velocity or0, dtime)endend
self.old_velocity_vector=velocityvec
self.old_velocity = gp.velocity
self.old_acceleration_vector=accelerationvec
self.old_yaw=yaw
atprintbm("wagon step", t)end)endfunction advtrains.get_real_path_index(train, pit)local pos_in_train_left=pit
local index=train.index
if pos_in_train_left>(index-math.floor(index))*(train.path_dist[math.floor(index)]or1)then
pos_in_train_left=pos_in_train_left - (index-math.floor(index))*(train.path_dist[math.floor(index)]or1)
index=math.floor(index)while pos_in_train_left>(train.path_dist[index-1]or1)do
pos_in_train_left=pos_in_train_left - (train.path_dist[index-1]or1)
index=index-1end
index=index-(pos_in_train_left/(train.path_dist[index-1]or1))else
index=index-(pos_in_train_left/(train.path_dist[math.floor(index-1)]or1))endreturn index
endfunction wagon:on_rightclick(clicker)return advtrains.pcall(function()if not self:ensure_init()then return endif not clicker or not clicker:is_player()thenreturnendif clicker:get_player_control().aux1 then--advtrains.dumppath(self:train().path)--minetest.chat_send_all("at index "..(self:train().index or "nil"))--advtrains.invert_train(self.train_id)atprint(dump(self))returnendlocal pname=clicker:get_player_name()local no=self:get_seatno(pname)if no thenif self.seat_groups thenlocal poss={}local sgr=self.seats[no].group
for _,access inipairs(self.seat_groups[sgr].access_to)doif self:check_seat_group_access(pname, access)then
poss[#poss+1]={name=self.seat_groups[access].name, key="sgr_"..access}endendif self.has_inventory and self.get_inventory_formspec then
poss[#poss+1]={name=attrans("Show Inventory"), key="inv"}endif self.seat_groups[sgr].driving_ctrl_access and minetest.check_player_privs(pname,"train_operator")then
poss[#poss+1]={name=attrans("Bord Computer"), key="bordcom"}endif self.owner==pname then
poss[#poss+1]={name=attrans("Wagon properties"), key="prop"}endif not self.seat_groups[sgr].require_doors_open or self:train().door_open~=0then
poss[#poss+1]={name=attrans("Get off"), key="off"}elseif clicker:get_player_control().sneak then
poss[#poss+1]={name=attrans("Get off (forced)"), key="off"}else
poss[#poss+1]={name=attrans("(Doors closed)"), key="dcwarn"}endendif#poss==0then--can't do anything.elseif#poss==1then
self:seating_from_key_helper(pname, {[poss[1].key]=true}, no)elselocal form ="size[5,"..1+(#poss).."]"for pos,ent inipairs(poss)do
form = form .."button_exit[0.5,"..(pos-0.5)..";4,1;"..ent.key..";"..ent.name.."]"end
minetest.show_formspec(pname,"advtrains_seating_"..self.unique_id, form)endelse
self:get_off(no)endelse--do not attach if already on a trainif advtrains.player_to_train_mapping[pname]then return endif self.seat_groups thenif#self.seats==0thenif self.has_inventory and self.get_inventory_formspec then
minetest.show_formspec(pname,"advtrains_inv_"..self.unique_id, self:get_inventory_formspec(pname))endreturnendlocal doors_open = self:train().door_open~=0or clicker:get_player_control().sneak
local allow, rsn=false,"unknown reason"for _,sgr inipairs(self.assign_to_seat_group)do
allow, rsn = self:check_seat_group_access(pname, sgr)if allow thenfor seatid, seatdef inipairs(self.seats)doif seatdef.group==sgr thenif(not self.seat_groups[sgr].require_doors_open or doors_open)thenif not self.seatp[seatid]then
self:get_on(clicker, seatid)returnelse
rsn="Wagon is full."endelse
rsn="Doors are closed! (try holding sneak key!)"endendendendend
minetest.chat_send_player(pname,attrans("Can't get on: "..rsn))else
self:show_get_on_form(pname)endendend)endfunction wagon:get_on(clicker, seatno)if not self.seatp then self.seatp={}endif not self.seatpc then self.seatpc={}end--player controls in driver standsif not self.seats[seatno]then return endlocal oldno=self:get_seatno(clicker:get_player_name())if oldno thenatprint("get_on: clearing oldno",seatno)
advtrains.player_to_train_mapping[clicker:get_player_name()]=nil
advtrains.clear_driver_hud(clicker:get_player_name())
self.seatp[oldno]=nilendif self.seatp[seatno]and self.seatp[seatno]~=clicker:get_player_name()thenatprint("get_on: throwing off",self.seatp[seatno],"from seat",seatno)
self:get_off(seatno)endatprint("get_on: attaching",clicker:get_player_name())
self.seatp[seatno] = clicker:get_player_name()
self.seatpc[seatno] = clicker:get_player_control_bits()
advtrains.player_to_train_mapping[clicker:get_player_name()]=self.train_id
clicker:set_attach(self.object,"", self.seats[seatno].attach_offset, {x=0,y=0,z=0})
clicker:set_eye_offset(self.seats[seatno].view_offset, self.seats[seatno].view_offset)endfunction wagon:get_off_plr(pname)local no=self:get_seatno(pname)if no then
self:get_off(no)endendfunction wagon:get_seatno(pname)for no, cont inpairs(self.seatp)doif cont==pname thenreturn no
endendreturn nilendfunction wagon:get_off(seatno)if not self.seatp[seatno]then return endlocal pname = self.seatp[seatno]local clicker = minetest.get_player_by_name(pname)
advtrains.player_to_train_mapping[pname]=nil
advtrains.clear_driver_hud(pname)
self.seatp[seatno]=nil
self.seatpc[seatno]=nilif clicker thenatprint("get_off: detaching",clicker:get_player_name())
clicker:set_detach()
clicker:set_eye_offset({x=0,y=0,z=0}, {x=0,y=0,z=0})local gp=self:train()--code as in step - automatic get onif self.door_entry and gp.door_open and gp.door_open~=0and gp.velocity==0and gp.index and gp.path thenlocal index=advtrains.get_real_path_index(gp, self.pos_in_train)--using the mapping created by the trainlogic globalstepfor i, ino inipairs(self.door_entry)dolocal aci = index + ino*(self.wagon_flipped and-1or1)local ix1=gp.path[math.floor(aci)]local ix2=gp.path[math.floor(aci+1)]-- the two wanted positions are ix1 and ix2 + (2nd-1st rotated by 90deg)-- (x z) rotated by 90deg is (-z x) (http://stackoverflow.com/a/4780141)-- multiplied by 2 here, to place off on platform, y of add is 1.local add = { x = (ix2.z-ix1.z)*gp.door_open, y =0, z = (ix1.x-ix2.x)*gp.door_open}local oadd = { x = (ix2.z-ix1.z)*gp.door_open*2, y =1, z = (ix1.x-ix2.x)*gp.door_open*2}local platpos=vector.round(vector.add(ix1, add))local offpos=vector.round(vector.add(ix1, oadd))atprint("platpos:", platpos,"offpos:", offpos)if minetest.get_item_group(minetest.get_node(platpos).name,"platform")>0then
minetest.after(0.2,function() clicker:setpos(offpos)end)returnendendelse--if not door_entry, or paths missing, fall back to old methodlocal objpos=advtrains.round_vector_floor_y(self.object:getpos())local yaw=self.object:getyaw()local isx=(yaw < math.pi/4)or(yaw >3*math.pi/4and yaw <5*math.pi/4)or(yaw >7*math.pi/4)--abuse helper functionfor _,r inipairs({-1,1})dolocal p=vector.add({x=isx and r or0, y=0, z=not isx and r or0}, objpos)local offp=vector.add({x=isx and r*2or0, y=1, z=not isx and r*2or0}, objpos)if minetest.get_item_group(minetest.get_node(p).name,"platform")>0then
minetest.after(0.2,function() clicker:setpos(offp)end)returnendendendendendfunction wagon:show_get_on_form(pname)if not self.initialized then return endif#self.seats==0thenif self.has_inventory and self.get_inventory_formspec then
minetest.show_formspec(pname,"advtrains_inv_"..self.unique_id, self:get_inventory_formspec(pname))endreturnendlocal form, comma="size[5,8]label[0.5,0.5;"..attrans("Select seat:").."]textlist[0.5,1;4,6;seat;",""for seatno, seattbl inipairs(self.seats)dolocal addtext, colorcode="",""if self.seatp and self.seatp[seatno]then
colorcode="#FF0000"
addtext=" ("..self.seatp[seatno]..")"end
form=form..comma..colorcode..seattbl.name..addtext
comma=","end
form=form..";0,false]"if self.has_inventory and self.get_inventory_formspec then
form=form.."button_exit[1,7;3,1;inv;"..attrans("Show Inventory").."]"end
minetest.show_formspec(pname,"advtrains_geton_"..self.unique_id, form)endfunction wagon:show_wagon_properties(pname)local numsgr=0if self.seat_groups then
numsgr=#self.seat_groups
endif not self.seat_access then
self.seat_access={}end--[[ fields: seat access: empty: everyone checkbox: lock couples button: save ]]local form="size[5,"..(numsgr*1.5+3).."]"local at=0if self.seat_groups thenfor sgr,sgrdef inpairs(self.seat_groups)dolocal text =attrans("Access to @1",sgrdef.name)
form=form.."field[0.5,"..(0.5+at*1.5)..";4,1;sgr_"..sgr..";"..text..";"..(self.seat_access[sgr]or"").."]"
at=at+1endend
form=form.."button_exit[0.5,"..(3+at*1.5)..";4,1;save;"..attrans("Save wagon properties").."]"
minetest.show_formspec(pname,"advtrains_prop_"..self.unique_id, form)end--BordComlocal functioncheckcouple(eid)if not eid then return nil endlocal ent=minetest.object_refs[eid]if not ent or not ent:getyaw()then
eid=nilreturn nilendlocal le = ent:get_luaentity()if not le or not le.is_couple then
eid=nilreturn nilendreturn le
endlocal functionchecklock(pname, own1, own2)return minetest.check_player_privs(pname,"train_remove")or((not own1 or own1==pname)or(not own2 or own2==pname))endfunction wagon:show_bordcom(pname)if not self:train()then return endlocal train = self:train()local form ="size[11,9]label[0.5,0;AdvTrains Boardcom v0.1]"
form=form.."textarea[0.5,1.5;7,1;text_outside;"..attrans("Text displayed outside on train")..";"..(train.text_outside or"").."]"
form=form.."textarea[0.5,3;7,1;text_inside;"..attrans("Text displayed inside train")..";"..(train.text_inside or"").."]"--row 5 : train overview and autocouplingif train.velocity==0then
form=form.."label[0.5,4.5;Train overview /coupling control:]"
linhei=5local pre_own, owns_any =nil, minetest.check_player_privs(pname,"train_remove")for i, tpid inipairs(train.trainparts)dolocal ent = advtrains.wagon_save[tpid]if ent thenlocal ename = ent.entity_name
form = form .."item_image["..i..","..linhei..";1,1;"..ename.."]"if i~=1thenif not ent.dcpl_lock then
form = form .."image_button["..(i-0.5)..","..(linhei+1)..";1,1;advtrains_discouple.png;dcpl_"..i..";]"ifchecklock(pname, ent.owner, pre_own)then
form = form .."image_button["..(i-0.5)..","..(linhei+2)..";1,1;advtrains_cpl_unlock.png;dcpl_lck_"..i..";]"endelse
form = form .."image_button["..(i-0.5)..","..(linhei+2)..";1,1;advtrains_cpl_lock.png;dcpl_ulck_"..i..";]"endendif i == self.pos_in_trainparts then
form = form .."box["..(i-0.1)..","..(linhei-0.1)..";1,1;green]"end
pre_own = ent.owner
owns_any = owns_any or(not ent.owner or ent.owner==pname)endendif train.movedir==1then
form = form .."label["..(#train.trainparts+1)..","..(linhei)..";-->]"else
form = form .."label[0.5,"..(linhei)..";<--]"end--check cpl_eid_front and _back of trainlocal couple_front =checkcouple(train.couple_eid_front)local couple_back =checkcouple(train.couple_eid_back)if couple_front then
form = form .."image_button[0.5,"..(linhei+1)..";1,1;advtrains_couple.png;cpl_f;]"endif couple_back then
form = form .."image_button["..(#train.trainparts+0.5)..","..(linhei+1)..";1,1;advtrains_couple.png;cpl_b;]"endif owns_any thenif train.couple_lck_front then
form = form .."image_button[0.5,"..(linhei+2)..";1,1;advtrains_cpl_lock.png;cpl_ulck_f;]"else
form = form .."image_button[0.5,"..(linhei+2)..";1,1;advtrains_cpl_unlock.png;cpl_lck_f;]"endif train.couple_lck_back then
form = form .."image_button["..(#train.trainparts+0.5)..","..(linhei+2)..";1,1;advtrains_cpl_lock.png;cpl_ulck_b;]"else
form = form .."image_button["..(#train.trainparts+0.5)..","..(linhei+2)..";1,1;advtrains_cpl_unlock.png;cpl_lck_b;]"endendelse
form=form.."label[0.5,4.5;Train overview / coupling control is only shown when the train stands.]"end
form = form .."button[0.5,8;3,1;Save;save]"
minetest.show_formspec(pname,"advtrains_bordcom_"..self.unique_id, form)endfunction wagon:handle_bordcom_fields(pname, formname, fields)local seatno=self:get_seatno(pname)if not seatno or not self.seat_groups[self.seats[seatno].group].driving_ctrl_access or not minetest.check_player_privs(pname,"train_operator")thenreturnendlocal train = self:train()if not train then return endif fields.text_outside thenif fields.text_outside~=""then
train.text_outside=fields.text_outside
else
train.text_outside=nilendendif fields.text_inside thenif fields.text_inside~=""then
train.text_inside=fields.text_inside
else
train.text_inside=nilendendfor i, tpid inipairs(train.trainparts)doif fields["dcpl_"..i]thenfor _,wagon inpairs(minetest.luaentities)doif wagon.is_wagon and wagon.initialized and wagon.unique_id==tpid then
wagon:safe_decouple(pname)endendendif i>1and fields["dcpl_lck_"..i]thenlocal ent = advtrains.wagon_save[tpid]local pent = advtrains.wagon_save[train.trainparts[i-1]]
if ent and pent thenifchecklock(pname, ent.owner, pent.owner)thenfor _,wagon inpairs(minetest.luaentities)doif wagon.is_wagon and wagon.initialized and wagon.unique_id==tpid then
wagon.dcpl_lock=true
wagon:get_staticdata()endendendendendif i>1and fields["dcpl_ulck_"..i]thenlocal ent = advtrains.wagon_save[tpid]local pent = advtrains.wagon_save[train.trainparts[i-1]]
if ent and pent thenifchecklock(pname, ent.owner, pent.owner)thenfor _,wagon inpairs(minetest.luaentities)doif wagon.is_wagon and wagon.initialized and wagon.unique_id==tpid then
wagon.dcpl_lock=false
wagon:get_staticdata()endendendendendend--check cpl_eid_front and _back of trainlocal couple_front =checkcouple(train.couple_eid_front)local couple_back =checkcouple(train.couple_eid_back)if fields.cpl_f and couple_front then
couple_front:on_rightclick(pname)endif fields.cpl_b and couple_back then
couple_back:on_rightclick(pname)endlocal functionchkownsany()local owns_any = minetest.check_player_privs(pname,"train_remove")for i, tpid inipairs(train.trainparts)dolocal ent = advtrains.wagon_save[tpid]if ent then
owns_any = owns_any or(not ent.owner or ent.owner==pname)endendreturn owns_any
endif fields.cpl_lck_f andchkownsany()then
train.couple_lck_front=trueendif fields.cpl_lck_b andchkownsany()then
train.couple_lck_back=trueendif fields.cpl_ulck_f andchkownsany()then
train.couple_lck_front=falseendif fields.cpl_ulck_b andchkownsany()then
train.couple_lck_back=falseendif not fields.quit then
self:show_bordcom(pname)endend
minetest.register_on_player_receive_fields(function(player, formname, fields)return advtrains.pcall(function()local uid=string.match(formname,"^advtrains_geton_(.+)$")if uid thenfor _,wagon inpairs(minetest.luaentities)doif wagon.is_wagon and wagon.initialized and wagon.unique_id==uid thenif fields.inv thenif wagon.has_inventory and wagon.get_inventory_formspec then
minetest.show_formspec(player:get_player_name(),"advtrains_inv_"..uid, wagon:get_inventory_formspec(player:get_player_name()))endelseif fields.seat thenlocal val=minetest.explode_textlist_event(fields.seat)if val and val.type~="INV"and not wagon.seatp[player:get_player_name()]then--get on
wagon:get_on(player, val.index)--will work with the new close_formspec functionality. close exactly this formspec.
minetest.show_formspec(player:get_player_name(), formname,"")endendendendend
uid=string.match(formname,"^advtrains_seating_(.+)$")if uid thenfor _,wagon inpairs(minetest.luaentities)doif wagon.is_wagon and wagon.initialized and wagon.unique_id==uid thenlocal pname=player:get_player_name()local no=wagon:get_seatno(pname)if no thenif wagon.seat_groups then
wagon:seating_from_key_helper(pname, fields, no)endendendendend
uid=string.match(formname,"^advtrains_prop_(.+)$")if uid thenfor _,wagon inpairs(minetest.luaentities)doif wagon.is_wagon and wagon.initialized and wagon.unique_id==uid thenlocal pname=player:get_player_name()if pname~=wagon.owner thenreturn trueendif fields.save or not fields.quit thenfor sgr,sgrdef inpairs(wagon.seat_groups)doif fields["sgr_"..sgr]thenlocal fcont = fields["sgr_"..sgr]
wagon.seat_access[sgr] = fcont~=""and fcont or nilendendendendendend
uid=string.match(formname,"^advtrains_bordcom_(.+)$")if uid thenfor _,wagon inpairs(minetest.luaentities)doif wagon.is_wagon and wagon.initialized and wagon.unique_id==uid then
wagon:handle_bordcom_fields(player:get_player_name(), formname, fields)endendendend)end)function wagon:seating_from_key_helper(pname, fields, no)local sgr=self.seats[no].group
for _,access inipairs(self.seat_groups[sgr].access_to)doif fields["sgr_"..access]and self:check_seat_group_access(pname, access)thenfor seatid, seatdef inipairs(self.seats)doif seatdef.group==access and not self.seatp[seatid]then
self:get_on(minetest.get_player_by_name(pname), seatid)returnendendendendif fields.inv and self.has_inventory and self.get_inventory_formspec then
minetest.show_formspec(player:get_player_name(),"advtrains_inv_"..self.unique_id, wagon:get_inventory_formspec(player:get_player_name()))endif fields.prop and self.owner==pname then
self:show_wagon_properties(pname)endif fields.bordcom and self.seat_groups[sgr].driving_ctrl_access and minetest.check_player_privs(pname,"train_operator")then
self:show_bordcom(pname)endif fields.dcwarn then
minetest.chat_send_player(pname,attrans("Doors are closed! Use Sneak+rightclick to ignore the closed doors and get off!"))endif fields.off then
self:get_off(no)endendfunction wagon:check_seat_group_access(pname, sgr)if self.seat_groups[sgr].driving_ctrl_access and not minetest.check_player_privs(pname,"train_operator")thenreturn false,"Missing train_operator privilege."endif not self.seat_access thenreturn trueendlocal sae=self.seat_access[sgr]if not sae or sae==""thenreturn trueendfor name instring.gmatch(sae,"%S+")doif name==pname thenreturn trueendendreturn false,"Blacklisted by owner."endfunction wagon:reattach_all()if not self.seatp then self.seatp={}endfor seatno, pname inpairs(self.seatp)dolocal p=minetest.get_player_by_name(pname)if p then
self:get_on(p ,seatno)endendendfunction wagon:safe_decouple(pname)if self.dcpl_lock then
minetest.chat_send_player(pname,"Couple is locked (ask owner or admin to unlock it)")return falseendatprint("wagon:discouple() Splitting train", selftrain_id)
advtrains.split_train_at_wagon(self)--found in trainlogic.luareturn trueendfunction advtrains.register_wagon(sysname_p, prototype, desc, inv_img)local sysname = sysname_p
if notstring.match(sysname,":")then
sysname ="advtrains:"..sysname_p
endsetmetatable(prototype, {__index=wagon})
minetest.register_entity(":"..sysname,prototype)
minetest.register_craftitem(":"..sysname, {
description = desc,
inventory_image = inv_img,
wield_image = inv_img,
stack_max =1,
on_place =function(itemstack, placer, pointed_thing)return advtrains.pcall(function()if not pointed_thing.type=="node"thenreturnendlocal node=minetest.get_node_or_nil(pointed_thing.under)if not node thenatprint("[advtrains]Ignore at placer position")return itemstack endlocal nodename=node.name
if(not advtrains.is_track_and_drives_on(nodename, prototype.drives_on))thenatprint("no track here, not placing.")return itemstack
endif not minetest.check_player_privs(placer, {train_place =true})and minetest.is_protected(pointed_thing.under, placer:get_player_name())then
minetest.record_protection_violation(pointed_thing.under, placer:get_player_name())returnendlocal tconns=advtrains.get_track_connections(node.name, node.param2)local yaw = placer:get_look_horizontal() + (math.pi/2)local plconnid = advtrains.yawToClosestConn(yaw, tconns)local prevpos = advtrains.get_adjacent_rail(pointed_thing.under, tconns, plconnid, prototype.drives_on)if not prevpos then return endlocal id=advtrains.create_new_train_at(pointed_thing.under, prevpos)local ob=minetest.add_entity(pointed_thing.under, sysname)if not ob thenatprint("couldn't add_entity, aborting")endlocal le=ob:get_luaentity()
le.owner=placer:get_player_name()local wagon_uid=le:init_new_instance(id, {})
advtrains.add_wagon_to_train(le, id)if not minetest.settings:get_bool("creative_mode")then
itemstack:take_item()endreturn itemstack
end)end,})end--[[ wagons can define update_animation(self, velocity) if they have a speed-dependent animation this function will be called when the velocity vector changes or every 2 seconds.]]