--[[Advanced Trains - Minetest ModCopyright (C) 2016-2020 Moritz Blei (orwell96) and contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.]]local lot = os.clock()
minetest.log("action","[advtrains] Loading...")-- Boilerplate to support localized strings if intllib mod is installed.if minetest.get_modpath("intllib")then
attrans = intllib.Getter()else
attrans =function(s,a,...)a={a,...}return s:gsub("@(%d+)",function(n)return a[tonumber(n)]end)endend--advtrains
DUMP_DEBUG_SAVE =false
GENERATE_ATRICIFIAL_LAG =false--Constant for maximum connection value/division of the circle
AT_CMAX =16
advtrains = {trains={}, player_to_train_mapping={}}--pcalllocal no_action=falselocal functionreload_saves()atwarn("Restoring saved state in 1 second...")
no_action=true
advtrains.lock_path_inval =false--read last save state and continue, as if server was restartedfor aoi, le inpairs(minetest.luaentities)doif le.is_wagon then
le.object:remove()endend
minetest.after(1,function()
advtrains.load()atwarn("Reload successful!")
advtrains.ndb.restore_all()end)endfunction advtrains.pcall(fun)if no_action then return endlocal succ, return1, return2, return3, return4=xpcall(fun,function(err)atwarn("Lua Error occured: ", err)atwarn(debug.traceback())if advtrains.atprint_context_tid then
advtrains.path_print(advtrains.trains[advtrains.atprint_context_tid], atdebug)atwarn(advtrains.trains[advtrains.atprint_context_tid].debug)endend)if not succ thenreload_saves()elsereturn return1, return2, return3, return4
endend
advtrains.modpath = minetest.get_modpath("advtrains")--Advtrains dump (special treatment of pos and sigd)functionatdump(t, intend)local str
iftype(t)=="table"thenif t.x and t.y and t.z then
str=minetest.pos_to_string(t)elseif t.p and t.s then-- interlocking sigd
str="S["..minetest.pos_to_string(t.p).."/"..t.s.."]"elseif advtrains.lines and t.s and t.m then-- RwT
str=advtrains.lines.rwt.to_string(t)else
str="{"local intd = (intend or"") .." "for k,v inpairs(t)doiftype(k)~="string"or notstring.match(k,"^path[_]?")then-- do not print anything path-related
str = str .."\n".. intd ..atdump(k, intd) .." = "..atdump(v, intd)endend
str = str .."\n".. (intend or"") .."}"endelseiftype(t)=="boolean"thenif t then
str="true"else
str="false"endelseiftype(t)=="function"then
str="<function>"elseiftype(t)=="userdata"then
str="<userdata>"else
str=""..t
endreturn str
endfunction advtrains.print_concat_table(a)local str=""local stra=""local t
for i=1,20do
t=a[i]if t==nil then
stra=stra.."nil "else
str=str..stra
stra=""
str=str..atdump(t).." "endendreturn str
end
atprint=function()end
atlog=function(t, ...)local text=advtrains.print_concat_table({t, ...})
minetest.log("action","[advtrains]"..text)end
atwarn=function(t, ...)local text=advtrains.print_concat_table({t, ...})
minetest.log("warning","[advtrains]"..text)
minetest.chat_send_all("[advtrains] -!- "..text)end
sid=function(id)if id then returnstring.sub(id, -6)end end--ONLY use this function for temporary debugging. for consistent debug prints use atprint
atdebug=function(t, ...)local text=advtrains.print_concat_table({t, ...})
minetest.log("action","[advtrains]"..text)
minetest.chat_send_all("[advtrains]"..text)endif minetest.settings:get_bool("advtrains_enable_debugging")then
atprint=function(t, ...)local context=advtrains.atprint_context_tid or""if not context then return endlocal text=advtrains.print_concat_table({t, ...})
advtrains.drb_record(context, text)--atlog("@@",advtrains.atprint_context_tid,t,...)enddofile(advtrains.modpath.."/debugringbuffer.lua")endfunctionassertt(var, typ)iftype(var)~=typ thenerror("Assertion failed, variable has to be of type "..typ)endenddofile(advtrains.modpath.."/helpers.lua");--dofile(advtrains.modpath.."/debugitems.lua");
advtrains.meseconrules ={{x=0, y=0, z=-1},{x=1, y=0, z=0},{x=-1, y=0, z=0},{x=0, y=0, z=1},{x=1, y=1, z=0},{x=1, y=-1, z=0},{x=-1, y=1, z=0},{x=-1, y=-1, z=0},{x=0, y=1, z=1},{x=0, y=-1, z=1},{x=0, y=1, z=-1},{x=0, y=-1, z=-1},{x=0, y=-2, z=0}}
advtrains.fpath=minetest.get_worldpath().."/advtrains"dofile(advtrains.modpath.."/path.lua")dofile(advtrains.modpath.."/trainlogic.lua")dofile(advtrains.modpath.."/trainhud.lua")dofile(advtrains.modpath.."/trackplacer.lua")dofile(advtrains.modpath.."/copytool.lua")dofile(advtrains.modpath.."/tracks.lua")dofile(advtrains.modpath.."/occupation.lua")dofile(advtrains.modpath.."/atc.lua")dofile(advtrains.modpath.."/wagons.lua")dofile(advtrains.modpath.."/protection.lua")dofile(advtrains.modpath.."/trackdb_legacy.lua")dofile(advtrains.modpath.."/nodedb.lua")dofile(advtrains.modpath.."/couple.lua")dofile(advtrains.modpath.."/signals.lua")dofile(advtrains.modpath.."/misc_nodes.lua")dofile(advtrains.modpath.."/crafting.lua")dofile(advtrains.modpath.."/craft_items.lua")dofile(advtrains.modpath.."/log.lua")dofile(advtrains.modpath.."/passive.lua")dofile(advtrains.modpath.."/lzb.lua")--load/save-- backup variables, used if someone should accidentally delete a sub-modlocal MDS_interlocking, MDS_lines
advtrains.fpath=minetest.get_worldpath().."/advtrains"dofile(advtrains.modpath.."/log.lua")function advtrains.read_component(name)local path = advtrains.fpath.."_"..name
minetest.log("action","[advtrains] loading "..path)local file, err = io.open(path,"r")if not file then
minetest.log("warning"," Failed to read advtrains save data from file "..path..": "..(err or"Unknown Error"))
minetest.log("warning"," (this is normal when first enabling advtrains on this world)")returnendlocal tbl = minetest.deserialize(file:read("*a"))
file:close()return tbl
endfunction advtrains.avt_load()-- check for new, split advtrains save filelocal version = advtrains.read_component("version")local tbl
if version and version ==3then-- we are dealing with the new, split-up system
minetest.log("action","[advtrains] loading savefiles version 3")local il_save = {
tcbs =true,
ts =true,
signalass =true,
rs_locks =true,
rs_callbacks =true,
influence_points =true,
npr_rails =true,}
tbl={
trains =true,
wagon_save =true,
ptmap =true,
atc =true,
ndb =true,
lines =true,
version =2,}for i,k inpairs(il_save)do
il_save[i] = advtrains.read_component("interlocking_"..i)endfor i,k inpairs(tbl)do
tbl[i] = advtrains.read_component(i)end
tbl["interlocking"] = il_save
elselocal file, err = io.open(advtrains.fpath,"r")if not file then
minetest.log("warning"," Failed to read advtrains save data from file "..advtrains.fpath..": "..(err or"Unknown Error"))
minetest.log("warning"," (this is normal when first enabling advtrains on this world)")returnelse
tbl = minetest.deserialize(file:read("*a"))
file:close()endendiftype(tbl) =="table"thenif tbl.version then--congrats, we have the new save format.
advtrains.trains = tbl.trains
--Save the train id into the train table to avoid having to pass id aroundfor id, train inpairs(advtrains.trains)do
train.id = id
end
advtrains.wagons = tbl.wagon_save
advtrains.player_to_train_mapping = tbl.ptmap or{}
advtrains.ndb.load_data(tbl.ndb)
advtrains.atc.load_data(tbl.atc)if advtrains.interlocking then
advtrains.interlocking.db.load(tbl.interlocking)else
MDS_interlocking = tbl.interlocking
endif advtrains.lines then
advtrains.lines.load(tbl.lines)else
MDS_lines = tbl.lines
end--remove wagon_save entries that are not part of a trainlocal todel=advtrains.merge_tables(advtrains.wagon_save)for tid, train inpairs(advtrains.trains)do
train.id = tid
for _, wid inipairs(train.trainparts)do
todel[wid]=nilendendfor wid, _ inpairs(todel)doatwarn("Removing unused wagon", wid,"from wagon_save table.")
advtrains.wagon_save[wid]=nilendelse--oh no, its the old one...
advtrains.trains=tbl
--load ATC
advtrains.fpath_atc=minetest.get_worldpath().."/advtrains_atc"local file, err = io.open(advtrains.fpath_atc,"r")if not file thenlocal er=err or"Unknown Error"atprint("Failed loading advtrains atc save file "..er)elselocal tbl = minetest.deserialize(file:read("*a"))iftype(tbl) =="table"then
advtrains.atc.controllers=tbl.controllers
end
file:close()end--load wagon saves
advtrains.fpath_ws=minetest.get_worldpath().."/advtrains_wagon_save"local file, err = io.open(advtrains.fpath_ws,"r")if not file thenlocal er=err or"Unknown Error"atprint("Failed loading advtrains save file "..er)elselocal tbl = minetest.deserialize(file:read("*a"))iftype(tbl) =="table"then
advtrains.wagon_save=tbl
end
file:close()endendelse
minetest.log("error"," Failed to deserialize advtrains save data: Not a table!")endend
advtrains.save_component =function(tbl, name)-- Saves each component of the advtrains file separately---- required for now to shrink the advtrains db to overcome lua-- limitations.local datastr = minetest.serialize(tbl)if not datastr then
minetest.log("error"," Failed to serialize advtrains save data!")returnendlocal path = advtrains.fpath.."_"..name
local success = minetest.safe_file_write(path, datastr)if not success then
minetest.log("error"," Failed to write advtrains save data to file "..path)endend
advtrains.avt_save =function(remove_players_from_wagons)--atprint("saving")if remove_players_from_wagons thenfor w_id, data inpairs(advtrains.wagons)do
data.seatp={}end
advtrains.player_to_train_mapping={}endlocal tmp_trains={}for id, train inpairs(advtrains.trains)do--first, deep_copy the trainif#train.trainparts >0thenlocal v=advtrains.save_keys(train, {"last_pos","last_connid","last_frac","velocity","tarvelocity","trainparts","recently_collided_with_env","atc_brake_target","atc_wait_finish","atc_command","atc_delay","door_open","text_outside","text_inside","line","routingcode","il_sections","speed_restriction","is_shunt",})--then save it
tmp_trains[id]=v
elseatwarn("Train",id,"had no wagons left because of some bug. It is being deleted. Wave it goodbye!")
advtrains.remove_train(id)endendfor id, wdata inpairs(advtrains.wagons)dolocal _,proto = advtrains.get_wagon_prototype(wdata)if proto.has_inventory thenlocal inv=minetest.get_inventory({type="detached", name="advtrains_wgn_"..id})if inv then-- inventory is not initialized when wagon was never loaded-- TOOD: What happens with unloading rails when they don't find the inventory?
wdata.ser_inv=advtrains.serialize_inventory(inv)endend-- TODO apply save-keys here too-- TODO temp
wdata.dcpl_lock =nilend--versions:-- 1 - Initial new save format.-- 2 - version as of tss branch 11-2018+local il_save
if advtrains.interlocking then
il_save = advtrains.interlocking.db.save()else
il_save = MDS_interlocking
endlocal ln_save
if advtrains.lines then
ln_save = advtrains.lines.save()else
ln_save = MDS_lines
endlocal save_tbl={
trains = tmp_trains,
wagon_save = advtrains.wagons,
ptmap = advtrains.player_to_train_mapping,
atc = advtrains.atc.save_data(),
ndb = advtrains.ndb.save_data(),
lines = ln_save,
version =3,}for i,k inpairs(save_tbl)do
advtrains.save_component(k,i)endfor i,k inpairs(il_save)do
advtrains.save_component(k,"interlocking_"..i)endif DUMP_DEBUG_SAVE thenlocal file, err = io.open(advtrains.fpath.."_DUMP","w")if err thenreturnend
file:write(dump(save_tbl))
file:close()endend--## MAIN LOOP ##----Calls all subsequent main tasks of both advtrains and atlatclocal init_load=falselocal save_interval=20local save_timer=save_interval
advtrains.mainloop_runcnt=0local t =0
minetest.register_globalstep(function(dtime_mt)return advtrains.pcall(function()
advtrains.mainloop_runcnt=advtrains.mainloop_runcnt+1--atprint("Running the main loop, runcnt",advtrains.mainloop_runcnt)--call load once. see advtrains.load() commentif not init_load then
advtrains.load()end--[[ local dtime if GENERATE_ATRICIFIAL_LAG then dtime = 0.2 if os.clock()<t then return end t = os.clock()+0.2 else --limit dtime: if trains move too far in one step, automation may cause stuck and wrongly braking trains dtime=dtime_mt if dtime>0.2 then atprint("Limiting dtime to 0.2!") dtime=0.2 end end ]]-- hotfix to prevent bugs related to ATC tracks.while(dtime_mt>0)dolocal dtime = dtime_mt
if dtime >0.2then dtime =0.2end
advtrains.mainloop_trainlogic(dtime)if advtrains_itm_mainloop thenadvtrains_itm_mainloop(dtime)endif atlatc then
atlatc.mainloop_stepcode(dtime)
atlatc.interrupt.mainloop(dtime)endif advtrains.lines then
advtrains.lines.step(dtime)end
dtime_mt = dtime_mt - dtime
end--trigger a save when necessary
save_timer=save_timer-dtime_mt
if save_timer<=0thenlocal t=os.clock()--save
advtrains.save()
save_timer=save_interval
atprintbm("saving", t)endend)end)--if something goes wrong in these functions, there is no help. no pcall here.--## MAIN LOAD ROUTINE ##-- Causes the loading of everything-- first time called in main loop (after the init phase) because luaautomation has to initialize first.function advtrains.load()
advtrains.avt_load()--loading advtrains. includes ndb at advtrains.ndb.load_data()if atlatc then
atlatc.load()--includes interruptsendif advtrains_itm_init thenadvtrains_itm_init()end
init_load=true
no_action=falseatlog("[load_all]Loaded advtrains save files")end--## MAIN SAVE ROUTINE ##-- Causes the saving of everythingfunction advtrains.save(remove_players_from_wagons)if not init_load then--wait... we haven't loaded yet?!atwarn("Instructed to save() but load() was never called!")returnend
advtrains.avt_save(remove_players_from_wagons)--saving advtrains. includes ndb at advtrains.ndb.save_data()if atlatc then
atlatc.save()endatprint("[save_all]Saved advtrains save files")--TODO very simple yet hacky workaround for the "green signals" bug
advtrains.invalidate_all_paths()end
minetest.register_on_shutdown(advtrains.save)-- This chat command provides a solution to the problem known on the LinuxWorks server-- There are many players that joined a single time, got on a train and then left forever-- These players still occupy seats in the trains.
minetest.register_chatcommand("at_empty_seats",{
params ="",-- Short parameter description
description ="Detach all players, especially the offline ones, from all trains. Use only when no one serious is on a train.",-- Full description
privs = {train_operator=true, server=true},-- Require the "privs" privilege to run
func =function(name, param)return advtrains.pcall(function()atwarn("Data is being saved. While saving, advtrains will remove the players from trains. Save files will be reloaded afterwards!")
advtrains.save(true)reload_saves()end)end,})-- This chat command solves another problem: Trains getting randomly stuck.
minetest.register_chatcommand("at_reroute",{
params ="",
description ="Delete all train routes, force them to recalculate",
privs = {train_operator=true},-- Only train operator is required, since this is relatively safe.
func =function(name, param)return advtrains.pcall(function()
advtrains.invalidate_all_paths()return true,"Successfully invalidated train routes"end)end,})local tot=(os.clock()-lot)*1000
minetest.log("action","[advtrains] Loaded in "..tot.."ms")