--[[
Advanced Trains - Minetest Mod
Copyright (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 .
]]
local lot = os.clock()
minetest.log("action", "[advtrains] Loading...")
--advtrains
advtrains = {trains={}, player_to_train_mapping={}}
-- =======================Development/debugging settings=====================
-- DO NOT USE FOR NORMAL OPERATION
local DUMP_DEBUG_SAVE = false
-- dump the save files in human-readable format into advtrains_DUMP
local GENERATE_ATRICIFIAL_LAG = false
local HOW_MANY_LAG = 1.0
-- Simulate a higher server step interval, as it occurs when the server is on high load
advtrains.IGNORE_WORLD = false
-- Run advtrains without respecting the world map
-- - No world collision checks occur
-- - The NDB forcibly places all nodes stored in it into the world regardless of the world's content.
-- - Rails do not set the 'attached_node' group
-- This mode can be useful for debugging/testing a world without the map data available
-- In this case, choose 'singlenode' as mapgen
local NO_SAVE = false
-- Do not save any data to advtrains save files
advtrains.TRAIN_MAX_WAGONS = 20
-- Limit on the maximum number of wagons that may be in a train
-- ==========================================================================
-- Use a global slowdown factor to slow down train movements. Now a setting
advtrains.DTIME_LIMIT = tonumber(minetest.settings:get("advtrains_dtime_limit")) or 0.2
advtrains.SAVE_INTERVAL = tonumber(minetest.settings:get("advtrains_save_interval")) or 60
--Constant for maximum connection value/division of the circle
AT_CMAX = 16
-- get wagon loading range
advtrains.wagon_load_range = tonumber(minetest.settings:get("advtrains_wagon_load_range"))
if not advtrains.wagon_load_range then
advtrains.wagon_load_range = tonumber(minetest.settings:get("active_block_range"))*16
end
--pcall
local no_action=false
local function reload_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 restarted
for aoi, le in pairs(minetest.luaentities) do
if le.is_wagon then
le.object:remove()
end
end
minetest.after(1, function()
advtrains.load()
atwarn("Reload successful!")
advtrains.ndb.restore_all()
end)
end
advtrains.modpath = minetest.get_modpath("advtrains")
--Advtrains dump (special treatment of pos and sigd)
function atdump(t, intend)
local str
if type(t)=="table" then
if 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 in pairs(t) do
if type(k)~="string" or not string.match(k, "^path[_]?") then
-- do not print anything path-related
str = str .. "\n" .. intd .. atdump(k, intd) .. " = " ..atdump(v, intd)
end
end
str = str .. "\n" .. (intend or "") .. "}"
end
elseif type(t)=="boolean" then
if t then
str="true"
else
str="false"
end
elseif type(t)=="function" then
str=""
elseif type(t)=="userdata" then
str=""
else
str=""..t
end
return str
end
function advtrains.print_concat_table(a)
local str=""
local stra=""
local t
for i=1,20 do
t=a[i]
if t==nil then
stra=stra.."nil "
else
str=str..stra
stra=""
str=str..atdump(t).." "
end
end
return 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 return string.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)
end
if minetest.settings:get_bool("advtrains_enable_debugging") then
atprint=function(t, ...)
local context=advtrains.atprint_context_tid or ""
if not context then return end
local text=advtrains.print_concat_table({t, ...})
advtrains.drb_record(context, text)
--atlog("@@",advtrains.atprint_context_tid,t,...)
end
dofile(advtrains.modpath.."/debugringbuffer.lua")
end
function assertt(var, typ)
if type(var)~=typ then
error("Assertion failed, variable has to be of type "..typ)
end
end
dofile(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"
advtrains.poconvert = dofile(advtrains.modpath.."/poconvert.lua")
advtrains.poconvert.from_flat("advtrains")
attrans = minetest.get_translator("advtrains")
advtrains.speed = dofile(advtrains.modpath.."/speed.lua")
advtrains.texture = dofile(advtrains.modpath.."/texture.lua")
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.."/wagonprop_tool.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")
if mesecon then
dofile(advtrains.modpath.."/p_mesecon_iface.lua")
end
dofile(advtrains.modpath.."/lzb.lua")
--load/save
-- backup variables, used if someone should accidentally delete a sub-mod
-- As of version 4, only used once during migration from version 3 to 4
-- Since version 4, each of the mods stores a separate save file.
local 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)")
return
end
local tbl = minetest.deserialize(file:read("*a"))
file:close()
return tbl
end
function advtrains.avt_load()
-- check for new, split advtrains save file
local version = advtrains.read_component("version")
local tbl
if version and version == 4 then
advtrains.load_version_4()
return
-- NOTE: From here, legacy loading code!
elseif version and version == 3 then
-- we are dealing with the 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 in pairs(il_save) do
il_save[i] = advtrains.read_component("interlocking_"..i)
end
for i,k in pairs(tbl) do
tbl[i] = advtrains.read_component(i)
end
tbl["interlocking"] = il_save
else
local 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)")
return
else
tbl = minetest.deserialize(file:read("*a"))
file:close()
end
end
if type(tbl) == "table" then
if 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 around
for id, train in pairs(advtrains.trains) do
train.id = id
end
advtrains.wagons = tbl.wagon_save
advtrains.player_to_train_mapping = tbl.ptmap or {}
advtrains.ndb.load_data_pre_v4(tbl.ndb)
advtrains.atc.load_data(tbl.atc)
if advtrains.interlocking then
advtrains.interlocking.db.load(tbl.interlocking)
else
MDS_interlocking = tbl.interlocking
end
if advtrains.lines then
advtrains.lines.load(tbl.lines)
else
MDS_lines = tbl.lines
end
--remove wagon_save entries that are not part of a train
local todel=advtrains.merge_tables(advtrains.wagon_save)
for tid, train in pairs(advtrains.trains) do
train.id = tid
for _, wid in ipairs(train.trainparts) do
todel[wid]=nil
end
end
for wid, _ in pairs(todel) do
atwarn("Removing unused wagon", wid, "from wagon_save table.")
advtrains.wagon_save[wid]=nil
end
else
--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 then
local er=err or "Unknown Error"
atprint("Failed loading advtrains atc save file "..er)
else
local tbl = minetest.deserialize(file:read("*a"))
if type(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 then
local er=err or "Unknown Error"
atprint("Failed loading advtrains save file "..er)
else
local tbl = minetest.deserialize(file:read("*a"))
if type(tbl) == "table" then
advtrains.wagon_save=tbl
end
file:close()
end
end
else
minetest.log("error", " Failed to deserialize advtrains save data: Not a table!")
end
-- moved from advtrains.load()
atlatc.load_pre_v4()
-- end of legacy loading code
end
function advtrains.load_version_4()
minetest.log("action", "[advtrains] loading savefiles version 4 (serialize_lib)")
--== load core ==
local at_save = serialize_lib.load_atomic(advtrains.fpath.."_core.ls")
if at_save then
advtrains.trains = at_save.trains
--Save the train id into the train table to avoid having to pass id around
for id, train in pairs(advtrains.trains) do
train.id = id
end
advtrains.wagons = at_save.wagons
advtrains.player_to_train_mapping = at_save.ptmap or {}
advtrains.atc.load_data(at_save.atc)
--remove wagon_save entries that are not part of a train
local todel=advtrains.merge_tables(advtrains.wagon_save)
for tid, train in pairs(advtrains.trains) do
train.id = tid
for _, wid in ipairs(train.trainparts) do
todel[wid]=nil
end
end
for wid, _ in pairs(todel) do
atwarn("Removing unused wagon", wid, "from wagon_save table.")
advtrains.wagon_save[wid]=nil
end
end
--== load ndb
serialize_lib.load_atomic(advtrains.fpath.."_ndb4.ls", advtrains.ndb.load_callback)
--== load interlocking ==
if advtrains.interlocking then
local il_save = serialize_lib.load_atomic(advtrains.fpath.."_interlocking.ls")
if il_save then
advtrains.interlocking.db.load(il_save)
end
end
--== load lines ==
if advtrains.lines then
local ln_save = serialize_lib.load_atomic(advtrains.fpath.."_lines.ls")
if ln_save then
advtrains.lines.load(ln_save)
end
end
--== load luaatc ==
if atlatc then
local la_save = serialize_lib.load_atomic(advtrains.fpath.."_atlatc.ls")
if la_save then
atlatc.load(la_save)
end
end
end
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.
-- Note: as of version 4, only used for the "advtrains_version" file
local datastr = minetest.serialize(tbl)
if not datastr then
minetest.log("error", " Failed to serialize advtrains save data!")
return
end
local 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)
end
end
advtrains.avt_save = function(remove_players_from_wagons)
--atdebug("Saving advtrains files (version 4)")
if remove_players_from_wagons then
for w_id, data in pairs(advtrains.wagons) do
data.seatp={}
end
advtrains.player_to_train_mapping={}
end
local tmp_trains={}
for id, train in pairs(advtrains.trains) do
--first, deep_copy the train
if #train.trainparts > 0 then
local 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", "speed_restrictions_t", "is_shunt",
"points_split", "autocouple", "atc_wait_autocouple", "ars_disable",
})
--then save it
tmp_trains[id]=v
else
atwarn("Train",id,"had no wagons left because of some bug. It is being deleted. Wave it goodbye!")
advtrains.remove_train(id)
end
end
for id, wdata in pairs(advtrains.wagons) do
local _,proto = advtrains.get_wagon_prototype(wdata)
if proto.has_inventory then
local 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)
end
end
-- TODO apply save-keys here too
-- TODO temp
wdata.dcpl_lock = nil
end
--versions:
-- 1 - Initial new save format.
-- 2 - version as of tss branch 11-2018+
-- 3 - split-up savefile system by gabriel
-- 4 - serialize_lib
-- save of core advtrains
local at_save={
trains = tmp_trains,
wagons = advtrains.wagons,
ptmap = advtrains.player_to_train_mapping,
atc = advtrains.atc.save_data(),
}
--save of interlocking
local il_save
if advtrains.interlocking then
il_save = advtrains.interlocking.db.save()
else
il_save = MDS_interlocking
end
-- save of lines
local ln_save
if advtrains.lines then
ln_save = advtrains.lines.save()
else
ln_save = MDS_lines
end
-- save of luaatc
local la_save
if atlatc then
la_save = atlatc.save()
end
-- parts table for serialize_lib API:
-- any table that is nil will not be included and thus not be overwritten
local parts_table = {
["core.ls"] = at_save,
["interlocking.ls"] = il_save,
["lines.ls"] = ln_save,
["atlatc.ls"] = la_save,
["ndb4.ls"] = true, -- data not used
}
local callbacks_table = {
["ndb4.ls"] = advtrains.ndb.save_callback
}
if DUMP_DEBUG_SAVE then
local file, err = io.open(advtrains.fpath.."_DUMP", "w")
if err then
return
end
file:write(dump(parts_table))
file:close()
end
--THE MAGIC HAPPENS HERE
local succ, err = serialize_lib.save_atomic_multiple(parts_table, advtrains.fpath.."_", callbacks_table)
if not succ then
atwarn("Saving failed: "..err)
else
-- store version
advtrains.save_component(4, "version")
end
end
--## MAIN LOOP ##--
--Calls all subsequent main tasks of both advtrains and atlatc
local init_load=false
local save_timer = advtrains.SAVE_INTERVAL
advtrains.mainloop_runcnt=0
advtrains.global_slowdown = 1
local t = 0
local within_mainstep = false
minetest.register_globalstep(function(dtime_mt)
if no_action then
-- the advtrains globalstep is skipped by command. Return immediately
return
end
within_mainstep = true
advtrains.mainloop_runcnt=advtrains.mainloop_runcnt+1
--atprint("Running the main loop, runcnt",advtrains.mainloop_runcnt)
--call load once. see advtrains.load() comment
if not init_load then
advtrains.load()
end
local dtime = dtime_mt * advtrains.global_slowdown
if GENERATE_ATRICIFIAL_LAG then
dtime = HOW_MANY_LAG
if os.clock() advtrains.DTIME_LIMIT then
if advtrains.global_slowdown > 0.1 then
advtrains.global_slowdown = advtrains.global_slowdown - 0.05
else
advtrains.global_slowdown = advtrains.global_slowdown / 2
end
dtime = advtrains.DTIME_LIMIT
end
-- recover global slowdown slowly over time
advtrains.global_slowdown = math.min(advtrains.global_slowdown*1.02, 1)
end
advtrains.mainloop_trainlogic(dtime,advtrains.mainloop_runcnt)
if advtrains_itm_mainloop then
advtrains_itm_mainloop(dtime)
end
if atlatc then
--atlatc.mainloop_stepcode(dtime)
atlatc.interrupt.mainloop(dtime)
end
if advtrains.lines then
advtrains.lines.step(dtime)
end
--trigger a save when necessary
save_timer=save_timer-dtime
if save_timer<=0 then
local t=os.clock()
--save
advtrains.save()
save_timer = advtrains.SAVE_INTERVAL
atprintbm("saving", t)
end
within_mainstep = false
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 interrupts
--end == No longer loading here. Now part of avt_save() legacy loading.
if advtrains_itm_init then
advtrains_itm_init()
end
init_load=true
no_action=false
atlog("[load_all]Loaded advtrains save files")
end
--## MAIN SAVE ROUTINE ##
-- Causes the saving of everything
function 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!")
return
end
if advtrains.IGNORE_WORLD then
advtrains.ndb.restore_all()
end
if NO_SAVE then
return
end
if no_action then
atlog("[save] Saving requested externally, but Advtrains step is disabled. Not saving any data as state may be inconsistent.")
return
end
local t1 = os.clock()
advtrains.avt_save(remove_players_from_wagons) --saving advtrains. includes ndb at advtrains.ndb.save_data()
if atlatc then
atlatc.save()
end
atlog("Saved advtrains save files, took",math.floor((os.clock()-t1) * 1000),"ms")
-- Cleanup actions
--TODO very simple yet hacky workaround for the "green signals" bug
advtrains.invalidate_all_paths()
end
minetest.register_on_shutdown(function()
if within_mainstep then
atwarn("Crash during advtrains main step - skipping the shutdown save operation to not save inconsistent data!")
else
advtrains.save()
end
end)
-- 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)
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,
})
-- 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)
advtrains.invalidate_all_paths()
return true, "Successfully invalidated train routes"
end,
})
minetest.register_chatcommand("at_whereis",
{
params = "",
description = "Returns the position of the train with the given id",
privs = {train_operator = true},
func = function(name,param)
local train = advtrains.trains[param]
if not train or not train.last_pos then
return false, "Train "..param.." does not exist or is invalid"
else
return true, "Train "..param.." is at "..minetest.pos_to_string(train.last_pos)
end
end,
})
minetest.register_chatcommand("at_tp",
{
params = "",
description = "Teleports you to the position of the train with the given id",
privs = {train_operator = true, teleport = true},
func = function(name,param)
local train = advtrains.trains[param]
if not train or not train.last_pos then
return false, "Train "..param.." does not exist or is invalid"
else
minetest.get_player_by_name(name):set_pos(train.last_pos)
return true, "Teleporting to train "..param
end
end,
})
minetest.register_chatcommand("at_disable_step",
{
params = "",
description = "Disable the advtrains globalstep temporarily",
privs = {server=true},
func = function(name, param)
if minetest.is_yes(param) then
-- disable everything, and turn off saving
no_action = true;
atwarn("The advtrains globalstep has been disabled. Trains are not moving, and no data is saved! Run '/at_disable_step no' to enable again!")
return true, "Disabled advtrains successfully"
elseif no_action then
atwarn("Re-enabling advtrains globalstep...")
reload_saves()
return true
else
return false, "Advtrains is already running normally!"
end
end,
})
minetest.register_chatcommand("at_status",
{
params = "",
description = "Print advtrains status info",
privs = {train_operator = true},
func = function(name, param)
return true, advtrains.print_concat_table({"Advtrains Status: no_action",no_action,"slowdown",advtrains.global_slowdown,"(log",math.log(advtrains.global_slowdown),")"})
end,
})
advtrains.is_no_action = function()
return no_action
end
local tot=(os.clock()-lot)*1000
minetest.log("action", "[advtrains] Loaded in "..tot.."ms")