From c3d04606a3b4da5181ce3467cb9e77d6a4e61b04 Mon Sep 17 00:00:00 2001 From: cheapie Date: Fri, 19 Apr 2024 11:24:33 -0500 Subject: [PATCH] Initial dispatcher work (not yet functional) --- controller.lua | 11 + controllerfw.lua | 42 +- dispatcher.lua | 436 ++++++++++++++++++ dispatcherfw.lua | 281 +++++++++++ init.lua | 1 + ...celevator_dispatcher_front_bottom_open.png | Bin 0 -> 1794 bytes textures/celevator_dispatcher_front_top.png | Bin 0 -> 7770 bytes .../celevator_dispatcher_front_top_open.png | Bin 0 -> 1539 bytes ...evator_dispatcher_front_top_open_lside.png | Bin 0 -> 8357 bytes 9 files changed, 768 insertions(+), 3 deletions(-) create mode 100644 dispatcher.lua create mode 100644 dispatcherfw.lua create mode 100644 textures/celevator_dispatcher_front_bottom_open.png create mode 100644 textures/celevator_dispatcher_front_top.png create mode 100644 textures/celevator_dispatcher_front_top_open.png create mode 100644 textures/celevator_dispatcher_front_top_open_lside.png diff --git a/controller.lua b/controller.lua index ce5c666..287983e 100644 --- a/controller.lua +++ b/controller.lua @@ -528,6 +528,17 @@ function celevator.controller.finish(pos,mem,changedinterrupts) if (mem.copformspec ~= oldmem.copformspec or mem.switchformspec ~= oldmem.switchformspec) and drivetype then minetest.after(0.25,celevator.drives[drivetype].updatecopformspec,drivepos) end + for _,message in ipairs(mem.messages) do + local destinfo = minetest.deserialize(celevator.storage:get_string(string.format("car%d",message.carid))) + if destinfo and destinfo.dispatcherpos then + celevator.dispatcher.run(destinfo.dispatcherpos,{ + type = "controllermsg", + source = mem.carid, + channel = message.channel, + msg = message.message, + }) + end + end meta:set_string("mem",minetest.serialize(mem)) if node.name == "celevator:controller_open" then meta:set_string("formspec",mem.formspec or "") end meta:set_string("formspec_hidden",mem.formspec or "") diff --git a/controllerfw.lua b/controllerfw.lua index 4fb93b3..e768efd 100644 --- a/controllerfw.lua +++ b/controllerfw.lua @@ -2,6 +2,16 @@ local pos,event,mem = ... local changedinterrupts = {} +mem.messages = {} + +local function send(carid,channel,message) + table.insert(mem.messages,{ + carid = carid, + channel = channel, + message = message, + }) +end + local function fault(ftype,fatal) if fatal then mem.fatalfault = true end if not mem.activefaults then mem.activefaults = {} end @@ -527,6 +537,31 @@ elseif event.type == "cartopbox" then pos = math.floor(mem.drive.status.apos)-1 }) end +elseif event.type == "dispatchermsg" then + if event.channel == "pairrequest" and mem.screenstate == "oobe_dispatcherconnect" then + mem.params.floornames = event.msg.floornames + mem.params.floorheights = event.msg.floorheights + mem.activefaults = {} + mem.faultlog = {} + mem.fatalfault = false + mem.state = "configured" + mem.screenstate = "status" + mem.screenpage = 1 + mem.carstate = "bfdemand" + if mem.doorstate == "closed" then + drivecmd({ + command = "setmaxvel", + maxvel = mem.params.contractspeed, + }) + drivecmd({command = "resetpos"}) + interrupt(0.1,"checkdrive") + mem.carmotion = true + juststarted = true + else + close() + end + send(event.source,"pairok",mem) + end end local oldstate = mem.carstate @@ -787,10 +822,11 @@ elseif mem.screenstate == "oobe_groupmode" then fs("button[1,3;2,1;simplex;Simplex]") fs("label[1,4.5;This will be the only elevator in the group. Hall calls will be handled by this controller.]") fs("button[1,6;2,1;group;Group]") - fs("label[1,7.5;This elevator will participate in a group with others. Hall calls will be handled by a dispatcher. (not implemented)]") + fs("label[1,7.5;This elevator will participate in a group with others. Hall calls will be handled by a dispatcher.]") elseif mem.screenstate == "oobe_dispatcherconnect" then - fs("button[1,10;2,1;back;< Back]") - fs("label[1,1;Not yet implemented. Press Back.]") + fs("button[1,10;2,1;back;< Cancel]") + fs("label[1,1;Waiting for connection from dispatcher...]") + fs(string.format("label[1,1.5;This controller's car ID is: %d]",mem.carid)) elseif mem.screenstate == "oobe_floortable" or mem.screenstate == "floortable" then if mem.screenstate == "oobe_floortable" then fs("label[1,1;Enter details of all floors this elevator will serve, then press Done.]") diff --git a/dispatcher.lua b/dispatcher.lua new file mode 100644 index 0000000..af29a84 --- /dev/null +++ b/dispatcher.lua @@ -0,0 +1,436 @@ +celevator.dispatcher = {} + +celevator.dispatcher.iqueue = minetest.deserialize(celevator.storage:get_string("dispatcher_iqueue")) or {} + +celevator.dispatcher.equeue = minetest.deserialize(celevator.storage:get_string("dispatcher_equeue")) or {} + +celevator.dispatcher.running = {} + +local fw,err = loadfile(minetest.get_modpath("celevator")..DIR_DELIM.."dispatcherfw.lua") +if not fw then error(err) end + +minetest.register_chatcommand("celevator_reloaddispatcher",{ + params = "", + description = "Reload celevator dispatcher firmware from disk", + privs = {server = true}, + func = function() + local newfw,loaderr = loadfile(minetest.get_modpath("celevator")..DIR_DELIM.."dispatcherfw.lua") + if newfw then + fw = newfw + return true,"Firmware reloaded successfully" + else + return false,loaderr + end + end, +}) + +local function after_place(pos,placer) + local node = minetest.get_node(pos) + local toppos = {x=pos.x,y=pos.y + 1,z=pos.z} + local topnode = minetest.get_node(toppos) + local placername = placer:get_player_name() + if topnode.name ~= "air" then + if placer:is_player() then + minetest.chat_send_player(placername,"Can't place cabinet - no room for the top half!") + end + minetest.set_node(pos,{name="air"}) + return true + end + if minetest.is_protected(toppos,placername) and not minetest.check_player_privs(placername,{protection_bypass=true}) then + if placer:is_player() then + minetest.chat_send_player(placername,"Can't place cabinet - top half is protected!") + minetest.record_protection_violation(toppos,placername) + end + minetest.set_node(pos,{name="air"}) + return true + end + node.name = "celevator:dispatcher_top" + minetest.set_node(toppos,node) +end + +local function ondestruct(pos) + pos.y = pos.y + 1 + local topnode = minetest.get_node(pos) + local dispatchertops = { + ["celevator:dispatcher_top"] = true, + ["celevator:dispatcher_top_open"] = true, + } + if dispatchertops[topnode.name] then + minetest.set_node(pos,{name="air"}) + end + celevator.dispatcher.equeue[minetest.hash_node_position(pos)] = nil + celevator.storage:set_string("dispatcher_equeue",minetest.serialize(celevator.dispatcher.equeue)) + local carid = minetest.get_meta(pos):get_int("carid") + if carid ~= 0 then celevator.storage:set_string(string.format("car%d",carid),"") end +end + +local function onrotate(controllerpos,node,user,mode,new_param2) + if not minetest.global_exists("screwdriver") then + return false + end + local ret = screwdriver.rotate_simple(controllerpos,node,user,mode,new_param2) + minetest.after(0,function(pos) + local newnode = minetest.get_node(pos) + local param2 = newnode.param2 + pos.y = pos.y + 1 + local topnode = minetest.get_node(pos) + topnode.param2 = param2 + minetest.set_node(pos,topnode) + end,controllerpos) + return ret +end + +local function handlefields(pos,_,fields,sender) + local playername = sender and sender:get_player_name() or "" + local event = {} + event.type = "ui" + event.fields = fields + event.sender = playername + celevator.dispatcher.run(pos,event) +end + +minetest.register_node("celevator:dispatcher",{ + description = "Elevator Dispatcher", + groups = { + cracky = 1, + }, + paramtype = "light", + paramtype2 = "facedir", + drawtype = "nodebox", + node_box = { + type = "fixed", + fixed = { + {-0.5,-0.5,0,0.5,0.5,0.5}, + }, + }, + selection_box = { + type = "fixed", + fixed = { + {-0.5,-0.5,0,0.5,1.5,0.5}, + }, + }, + tiles = { + "celevator_cabinet_sides.png", + "celevator_cabinet_sides.png", + "celevator_cabinet_sides.png", + "celevator_cabinet_sides.png", + "celevator_cabinet_sides.png", + "celevator_cabinet_front_bottom.png", + }, + after_place_node = after_place, + on_destruct = ondestruct, + on_rotate = onrotate, + on_receive_fields = handlefields, + on_construct = function(pos) + local meta = minetest.get_meta(pos) + meta:set_string("mem",minetest.serialize({})) + meta:mark_as_private("mem") + local event = {} + event.type = "program" + local carid = celevator.storage:get_int("maxcarid")+1 + meta:set_int("carid",carid) + celevator.storage:set_int("maxcarid",carid) + celevator.storage:set_string(string.format("car%d",carid),minetest.serialize({dispatcherpos=pos,callbuttons={},fs1switches={}})) + celevator.dispatcher.run(pos,event) + end, + on_punch = function(pos,node,puncher) + if not puncher:is_player() then + return + end + local name = puncher:get_player_name() + if minetest.is_protected(pos,name) and not minetest.check_player_privs(name,{protection_bypass=true}) then + minetest.chat_send_player(name,"Can't open cabinet - cabinet is locked.") + minetest.record_protection_violation(pos,name) + return + end + node.name = "celevator:dispatcher_open" + minetest.swap_node(pos,node) + local meta = minetest.get_meta(pos) + meta:set_string("formspec",meta:get_string("formspec_hidden")) + pos.y = pos.y + 1 + node = minetest.get_node(pos) + node.name = "celevator:dispatcher_top_open" + minetest.swap_node(pos,node) + minetest.sound_play("doors_steel_door_open",{ + pos = pos, + gain = 0.5, + max_hear_distance = 10 + },true) + end, +}) + +minetest.register_node("celevator:dispatcher_open",{ + description = "Dispatcher (door open - you hacker you!)", + groups = { + cracky = 1, + not_in_creative_inventory = 1, + }, + paramtype = "light", + paramtype2 = "facedir", + drawtype = "nodebox", + drop = "celevator:dispatcher", + node_box = { + type = "fixed", + fixed = { + {-0.5,-0.5,0,0.5,0.5,0.5}, + {-0.5,-0.5,-0.5,-0.45,0.5,0}, + {0.45,-0.5,-0.5,0.5,0.5,0}, + }, + }, + selection_box = { + type = "fixed", + fixed = { + {-0.5,-0.5,-0.5,0.5,1.5,0.5}, + }, + }, + tiles = { + "celevator_cabinet_sides.png", + "celevator_cabinet_sides.png", + "celevator_cabinet_front_bottom_open_rside.png", + "celevator_cabinet_front_bottom_open_lside.png", + "celevator_cabinet_sides.png", + { + name="celevator_dispatcher_front_bottom_open.png", + animation={type="vertical_frames", aspect_w=32, aspect_h=32, length=2}, + } + }, + after_place_node = after_place, + on_destruct = ondestruct, + on_rotate = onrotate, + on_receive_fields = handlefields, + on_punch = function(pos,node,puncher) + if not puncher:is_player() then + return + end + node.name = "celevator:dispatcher" + minetest.swap_node(pos,node) + local meta = minetest.get_meta(pos) + meta:set_string("formspec","") + pos.y = pos.y + 1 + node = minetest.get_node(pos) + node.name = "celevator:dispatcher_top" + minetest.swap_node(pos,node) + minetest.sound_play("doors_steel_door_close",{ + pos = pos, + gain = 0.5, + max_hear_distance = 10 + },true) + end, +}) + +minetest.register_node("celevator:dispatcher_top",{ + description = "Dispatcher (top section - you hacker you!)", + groups = { + not_in_creative_inventory = 1, + }, + drop = "", + paramtype = "light", + paramtype2 = "facedir", + drawtype = "nodebox", + node_box = { + type = "fixed", + fixed = { + {-0.5,-0.5,0,0.5,0.5,0.5}, + }, + }, + selection_box = { + type = "fixed", + fixed = { + {0,0,0,0,0,0}, + }, + }, + tiles = { + "celevator_cabinet_sides.png", + "celevator_cabinet_sides.png", + "celevator_cabinet_sides.png", + "celevator_cabinet_sides.png", + "celevator_cabinet_sides.png", + "celevator_dispatcher_front_top.png", + }, +}) + +minetest.register_node("celevator:dispatcher_top_open",{ + description = "Dispatcher (top section, open - you hacker you!)", + groups = { + not_in_creative_inventory = 1, + }, + drop = "", + paramtype = "light", + paramtype2 = "facedir", + drawtype = "nodebox", + node_box = { + type = "fixed", + fixed = { + {-0.5,-0.5,0,0.5,0.5,0.5}, + {-0.5,-0.5,-0.5,-0.45,0.5,0}, + {0.45,-0.5,-0.5,0.5,0.5,0}, + }, + }, + selection_box = { + type = "fixed", + fixed = { + {0,0,0,0,0,0}, + }, + }, + tiles = { + "celevator_cabinet_sides.png", + "celevator_cabinet_sides.png", + "celevator_cabinet_front_top_open_rside.png", + "celevator_dispatcher_front_top_open_lside.png", + "celevator_cabinet_sides.png", + "celevator_dispatcher_front_top_open.png", + }, +}) + +function celevator.dispatcher.isdispatcher(pos) + local node = celevator.get_node(pos) + return (node.name == "celevator:dispatcher" or node.name == "celevator:dispatcher_open") +end + +function celevator.dispatcher.finish(pos,mem,changedinterrupts) + if not celevator.dispatcher.isdispatcher(pos) then + return + else + local meta = minetest.get_meta(pos) + local carid = meta:get_int("carid") + local carinfo = minetest.deserialize(celevator.storage:get_string(string.format("car%d",carid))) + local carinfodirty = false + if not carinfo then + minetest.log("error","[celevator] [controller] Bad car info for dispatcher at "..minetest.pos_to_string(pos)) + return + end + local node = celevator.get_node(pos) + local oldmem = minetest.deserialize(meta:get_string("mem")) or {} + local oldupbuttonlights = oldmem.upcalls or {} + local olddownbuttonlights = oldmem.dncalls or {} + local newupbuttonlights = mem.upcalls or {} + local newdownbuttonlights = mem.dncalls or {} + local callbuttons = carinfo.callbuttons + for _,button in pairs(callbuttons) do + if oldupbuttonlights[button.landing] ~= newupbuttonlights[button.landing] then + celevator.callbutton.setlight(button.pos,"up",newupbuttonlights[button.landing]) + end + if olddownbuttonlights[button.landing] ~= newdownbuttonlights[button.landing] then + celevator.callbutton.setlight(button.pos,"down",newdownbuttonlights[button.landing]) + end + end + local oldfs1led = oldmem.fs1led + local newfs1led = mem.fs1led + local fs1switches = carinfo.fs1switches or {} + if oldfs1led ~= newfs1led then + for _,fs1switch in pairs(fs1switches) do + celevator.fs1switch.setled(fs1switch.pos,newfs1led) + end + end + for _,message in ipairs(mem.messages) do + local destinfo = minetest.deserialize(celevator.storage:get_string(string.format("car%d",message.carid))) + if destinfo and destinfo.controllerpos then + celevator.controller.run(destinfo.controllerpos,{ + type = "dispatchermsg", + source = mem.carid, + channel = message.channel, + msg = message.message, + }) + end + end + meta:set_string("mem",minetest.serialize(mem)) + if node.name == "celevator:dispatcher_open" then meta:set_string("formspec",mem.formspec or "") end + meta:set_string("formspec_hidden",mem.formspec or "") + meta:set_string("infotext",mem.infotext or "") + local hash = minetest.hash_node_position(pos) + if not celevator.dispatcher.iqueue[hash] then celevator.dispatcher.iqueue[hash] = mem.interrupts end + for iid in pairs(changedinterrupts) do + celevator.dispatcher.iqueue[hash][iid] = mem.interrupts[iid] + end + celevator.storage:set_string("dispatcher_iqueue",minetest.serialize(celevator.dispatcher.iqueue)) + celevator.dispatcher.running[hash] = nil + if #celevator.dispatcher.equeue[hash] > 0 then + local event = celevator.dispatcher.equeue[hash][1] + table.remove(celevator.dispatcher.equeue[hash],1) + celevator.storage:set_string("dispatcher_equeue",minetest.serialize(celevator.dispatcher.equeue)) + celevator.dispatcher.run(pos,event) + end + if carinfodirty then + celevator.storage:set_string(string.format("car%d",carid),minetest.serialize(carinfo)) + end + end +end + +function celevator.dispatcher.run(pos,event) + if not celevator.dispatcher.isdispatcher(pos) then + return + else + local hash = minetest.hash_node_position(pos) + if not celevator.dispatcher.equeue[hash] then + celevator.dispatcher.equeue[hash] = {} + celevator.storage:set_string("dispatcher_equeue",minetest.serialize(celevator.dispatcher.equeue)) + end + if celevator.dispatcher.running[hash] then + table.insert(celevator.dispatcher.equeue[hash],event) + celevator.storage:set_string("dispatcher_equeue",minetest.serialize(celevator.dispatcher.equeue)) + if #celevator.dispatcher.equeue[hash] > 5 then + local message = "[celevator] [dispatcher] Async process for dispatcher at %s is falling behind, %d events in queue" + minetest.log("warning",string.format(message,minetest.pos_to_string(pos),#celevator.dispatcher.equeue[hash])) + end + return + end + celevator.dispatcher.running[hash] = true + local meta = minetest.get_meta(pos) + local mem = minetest.deserialize(meta:get_string("mem")) + if not mem then + minetest.log("error","[celevator] [controller] Failed to load dispatcher memory at "..minetest.pos_to_string(pos)) + return + end + mem.interrupts = celevator.dispatcher.iqueue[minetest.hash_node_position(pos)] or {} + mem.carid = meta:get_int("carid") + minetest.handle_async(fw,celevator.dispatcher.finish,pos,event,mem) + end +end + +function celevator.dispatcher.handlecallbutton(dispatcherpos,landing,dir) + local event = { + type = "callbutton", + landing = landing, + dir = dir, + } + celevator.dispatcher.run(dispatcherpos,event) +end + +function celevator.controller.handlefs1switch(dispatcherpos,on) + local event = { + type = "fs1switch", + state = on, + } + celevator.dispatcher.run(dispatcherpos,event) +end + +function celevator.dispatcher.checkiqueue(dtime) + for hash,iqueue in pairs(celevator.dispatcher.iqueue) do + local pos = minetest.get_position_from_hash(hash) + for iid,time in pairs(iqueue) do + iqueue[iid] = time-dtime + if iqueue[iid] < 0 then + iqueue[iid] = nil + local event = {} + event.type = "interrupt" + event.iid = iid + celevator.dispatcher.run(pos,event) + end + end + end +end + +minetest.register_globalstep(celevator.dispatcher.checkiqueue) + +minetest.register_abm({ + label = "Run otherwise idle dispatchers if a user is nearby", + nodenames = {"celevator:dispatcher","celevator:dispatcher_open"}, + interval = 1, + chance = 1, + action = function(pos) + local event = { + type = "abm" + } + celevator.dispatcher.run(pos,event) + end, +}) diff --git a/dispatcherfw.lua b/dispatcherfw.lua new file mode 100644 index 0000000..15650df --- /dev/null +++ b/dispatcherfw.lua @@ -0,0 +1,281 @@ +local pos,event,mem = ... + +local changedinterrupts = {} + +local function interrupt(time,iid) + mem.interrupts[iid] = time + changedinterrupts[iid] = true +end + +mem.messages = {} + +local function send(carid,channel,message) + table.insert(mem.messages,{ + carid = carid, + channel = channel, + message = message, + }) +end + +mem.formspec = "" + +local function fs(element) + mem.formspec = mem.formspec..element +end + +if event.type == "program" then + mem.carstatus = {} + mem.screenstate = "oobe_welcome" + mem.editingfloor = 1 + mem.screenpage = 1 + mem.editingconnection = 1 + mem.newconncarid = 0 + if not mem.params then + mem.params = { + carids = {}, + floorheights = {5,5,5}, + floornames = {"1","2","3"}, + floorsserved = {}, + } + end +elseif event.type == "ui" then + local fields = event.fields + if mem.screenstate == "oobe_welcome" then + if fields.license then + mem.screenstate = "oobe_license" + elseif fields.next then + mem.screenstate = "oobe_floortable" + end + elseif mem.screenstate == "oobe_license" then + if fields.back then + mem.screenstate = "oobe_welcome" + end + elseif mem.screenstate == "oobe_floortable" or mem.screenstate == "floortable" then + local exp = event.fields.floor and minetest.explode_textlist_event(event.fields.floor) or {} + if event.fields.back then + mem.screenstate = "oobe_welcome" + elseif event.fields.next then + mem.screenstate = (mem.screenstate == "oobe_floortable" and "oobe_connections" or "parameters") + mem.screenpage = 1 + elseif exp.type == "CHG" then + mem.editingfloor = #mem.params.floornames-exp.index+1 + elseif exp.type == "DCL" then + mem.editingfloor = #mem.params.floornames-exp.index+1 + mem.screenstate = (mem.screenstate == "oobe_floortable" and "oobe_floortable_edit" or "floortable_edit") + elseif event.fields.edit then + mem.screenstate = (mem.screenstate == "oobe_floortable" and "oobe_floortable_edit" or "floortable_edit") + elseif event.fields.add then + table.insert(mem.params.floorheights,5) + table.insert(mem.params.floornames,tostring(#mem.params.floornames+1)) + elseif event.fields.remove then + table.remove(mem.params.floorheights,mem.editingfloor) + table.remove(mem.params.floornames,mem.editingfloor) + mem.editingfloor = math.max(1,mem.editingfloor-1) + elseif event.fields.moveup then + local height = mem.params.floorheights[mem.editingfloor] + local name = mem.params.floornames[mem.editingfloor] + table.remove(mem.params.floorheights,mem.editingfloor) + table.remove(mem.params.floornames,mem.editingfloor) + table.insert(mem.params.floorheights,mem.editingfloor+1,height) + table.insert(mem.params.floornames,mem.editingfloor+1,name) + mem.editingfloor = mem.editingfloor + 1 + elseif event.fields.movedown then + local height = mem.params.floorheights[mem.editingfloor] + local name = mem.params.floornames[mem.editingfloor] + table.remove(mem.params.floorheights,mem.editingfloor) + table.remove(mem.params.floornames,mem.editingfloor) + table.insert(mem.params.floorheights,mem.editingfloor-1,height) + table.insert(mem.params.floornames,mem.editingfloor-1,name) + mem.editingfloor = mem.editingfloor - 1 + end + elseif mem.screenstate == "oobe_floortable_edit" or mem.screenstate == "floortable_edit" then + if event.fields.back or event.fields.save then + mem.screenstate = (mem.screenstate == "oobe_floortable_edit" and "oobe_floortable" or "floortable") + local height = tonumber(event.fields.height) + if height then + height = math.floor(height+0.5) + mem.params.floorheights[mem.editingfloor] = math.max(0,height) + end + mem.params.floornames[mem.editingfloor] = string.sub(event.fields.name,1,256) + end + elseif mem.screenstate == "oobe_connections" or mem.screenstate == "connections" then + local exp = event.fields.connection and minetest.explode_textlist_event(event.fields.connection) or {} + if event.fields.back then + mem.screenstate = "oobe_floortable" + elseif event.fields.next and #mem.params.carids > 0 then + mem.screenstate = (mem.screenstate == "oobe_connections" and "status" or "parameters") + mem.screenpage = 1 + elseif exp.type == "CHG" then + mem.editingconnection = #mem.params.carids-exp.index+1 + elseif exp.type == "DCL" then + mem.editingconnection = #mem.params.carids-exp.index+1 + mem.screenstate = (mem.screenstate == "oobe_connections" and "oobe_connection" or "connection") + elseif event.fields.edit then + mem.screenstate = (mem.screenstate == "oobe_connections" and "oobe_connection" or "connection") + elseif event.fields.add then + mem.newconnfloors = {} + for i in ipairs(mem.params.floornames) do + mem.newconnfloors[i] = true + end + mem.screenstate = (mem.screenstate == "oobe_connections" and "oobe_newconnection" or "newconnection") + elseif event.fields.remove then + mem.carstatus[mem.params.carids[mem.editingconnection]] = nil + table.remove(mem.params.carids,mem.editingconnection) + mem.editingconnection = math.max(1,mem.editingconnection-1) + end + elseif mem.screenstate == "oobe_newconnection" or mem.screenstate == "newconnection" then + local exp = event.fields.floors and minetest.explode_textlist_event(event.fields.floors) or {} + if event.fields.back then + mem.screenstate = (mem.screenstate == "oobe_newconnection" and "oobe_connections" or "connections") + elseif event.fields.connect and fields.carid and tonumber(fields.carid) then + mem.screenstate = (mem.screenstate == "oobe_newconnection" and "oobe_connecting" or "connecting") + local floornames = {} + local floorheights = {} + for i=1,#mem.params.floornames,1 do + if mem.newconnfloors[i] then + table.insert(floornames,mem.params.floornames[i]) + table.insert(floorheights,mem.params.floorheights[i]) + elseif #floornames > 0 then + floorheights[#floorheights] = floorheights[#floorheights]+mem.params.floorheights[i] + end + end + send(tonumber(fields.carid),"pairrequest",{ + dispatcherid = mem.carid, + floornames = floornames, + floorheights = floorheights, + }) + interrupt(3,"connecttimeout") + elseif exp.type == "CHG" then + local floor = #mem.params.floornames-exp.index+1 + mem.newconnfloors[floor] = not mem.newconnfloors[floor] + end + elseif mem.screenstate == "oobe_connectionfailed" or mem.screenstate == "connectionfailed" then + if fields.back then + mem.screenstate = (mem.screenstate == "oobe_connectionfailed" and "oobe_newconnection" or "newconnection") + end + end +elseif event.iid == "connecttimeout" then + if mem.screenstate == "oobe_connecting" then + mem.screenstate = "oobe_connectionfailed" + elseif mem.screenstate == "connecting" then + mem.screenstate = "connectionfailed" + end +elseif event.channel == "pairok" then + if mem.screenstate == "oobe_connecting" or mem.screenstate == "connecting" then + interrupt(nil,"connecttimeout") + mem.screenstate = (mem.screenstate == "oobe_connecting" and "oobe_connections" or "connections") + mem.carstatus[event.source] = { + upcalls = {}, + dncalls = {}, + carcalls = {}, + position = event.msg.drive.status.apos or 0, + state = event.msg.carstate, + } + mem.params.floorsserved[event.source] = mem.newconnfloors + table.insert(mem.params.carids,event.source) + end +end + +fs("formspec_version[6]") +fs("size[16,12]") +fs("background9[0,0;16,12;celevator_fs_bg.png;true;3]") + +if mem.screenstate == "oobe_welcome" then + fs("image[6,1;4,2;celevator_logo.png]") + fs("label[1,4;Welcome to your new MTronic XT elevator dispatcher!]") + fs("label[1,4.5;Before continuing, make sure you have at least two controllers in group operation mode and ready to connect.]") + fs("label[1,5.5;Press Next to begin.]") + fs("button[1,10;2,1;license;License Info]") + fs("button[13,10;2,1;next;Next >]") +elseif mem.screenstate == "oobe_license" then + local licensefile = io.open(minetest.get_modpath("celevator")..DIR_DELIM.."COPYING") + local license = minetest.formspec_escape(licensefile:read("*all")) + licensefile:close() + fs("textarea[1,1;14,8;license;This applies to the whole celevator mod\\, not just this dispatcher:;"..license.."]") + fs("button[7,10.5;2,1;back;OK]") +elseif mem.screenstate == "oobe_floortable" or mem.screenstate == "floortable" then + if mem.screenstate == "oobe_floortable" then + fs("label[1,1;Enter details of all floors this group will serve, then press Next.]") + fs("label[1,1.3;Include all floors served by any car in the group, even if not served by all cars.]") + fs("button[1,10;2,1;back;< Back]") + fs("button[13,10;2,1;next;Next >]") + else + fs("label[1,1;EDIT FLOOR TABLE]") + fs("button[1,10;2,1;next;Done]") + end + fs("textlist[1,2;6,7;floor;") + for i=#mem.params.floornames,1,-1 do + fs(minetest.formspec_escape(string.format("%d - Height: %d - PI: %s",i,mem.params.floorheights[i],mem.params.floornames[i]))..(i==1 and "" or ",")) + end + fs(";"..tostring(#mem.params.floornames-mem.editingfloor+1)..";false]") + if #mem.params.floornames < 100 then fs("button[8,2;2,1;add;New Floor]") end + fs("button[8,3.5;2,1;edit;Edit Floor]") + if #mem.params.floornames > 2 then fs("button[8,5;2,1;remove;Remove Floor]") end + if mem.editingfloor < #mem.params.floornames then fs("button[8,6.5;2,1;moveup;Move Up]") end + if mem.editingfloor > 1 then fs("button[8,8;2,1;movedown;Move Down") end +elseif mem.screenstate == "oobe_floortable_edit" or mem.screenstate == "floortable_edit" then + if mem.screenstate == "oobe_floortable_edit" then + fs("button[7,10.5;2,1;back;OK]") + fs("label[1,5;The Floor Height is the distance (in meters/nodes) from the floor level of this floor to the floor level of the next floor.]") + fs("label[1,5.5;(not used at the highest floor)]") + fs("label[1,6.5;The Floor Name is how the floor will be displayed on the position indicators.]") + else + fs("button[7,10.5;2,1;save;Save]") + end + fs("label[1,1;Editing floor "..tostring(mem.editingfloor).."]") + fs("field[1,3;3,1;height;Floor Height;"..tostring(mem.params.floorheights[mem.editingfloor]).."]") + fs("field[5,3;3,1;name;Floor Name;"..minetest.formspec_escape(mem.params.floornames[mem.editingfloor]).."]") +elseif mem.screenstate == "oobe_connections" or mem.screenstate == "connections" then + if mem.screenstate == "oobe_connections" then + fs("label[1,1;Connect to each car in the group, then click Done.]") + fs("button[1,10;2,1;back;< Back]") + if #mem.params.carids > 0 then fs("button[13,10;2,1;next;Done >]") end + else + fs("label[1,1;EDIT CONNECTIONS]") + if #mem.params.carids > 0 then fs("button[1,10;2,1;next;Done]") end + end + if #mem.params.carids > 0 then + fs("textlist[1,2;6,7;connection;") + for i=#mem.params.carids,1,-1 do + fs(string.format("Car %d - ID #%d",i,mem.params.carids[i])..(i==1 and "" or ",")) + end + fs(";"..tostring(#mem.params.floornames-mem.editingfloor+1)..";false]") + else + fs("label[1,2;No Connections]") + end + if #mem.params.carids < 16 then fs("button[8,2;2,1;add;New Connection]") end + if #mem.params.carids > 0 then fs("button[8,3.5;2,1;edit;Edit Connection]") end + if #mem.params.carids > 0 then fs("button[8,5;2,1;remove;Remove Connection]") end +elseif mem.screenstate == "oobe_newconnection" or mem.screenstate == "newconnection" then + local numfloors = 0 + for _,v in ipairs(mem.newconnfloors) do + if v then numfloors = numfloors + 1 end + end + if mem.screenstate == "oobe_newconnection" then + fs("label[1,1;Enter the car ID and select the floors served (click them to toggle), then click Connect.]") + fs("label[1,1.3;You must select at least two floors.]") + fs("button[1,10;2,1;back;< Back]") + if numfloors >= 2 then fs("button[13,10;2,1;connect;Connect >]") end + else + fs("label[1,1;NEW CONNECTION]") + fs("button[1,10;2,1;back;< Back]") + if numfloors >= 2 then fs("button[13,10;2,1;connect;Connect >]") end + end + fs("textlist[8,2;6,7;floors;") + for i=#mem.params.floornames,1,-1 do + fs(string.format("%s - %s",minetest.formspec_escape(mem.params.floornames[i]),mem.newconnfloors[i] and "YES" or "NO")..(i==1 and "" or ",")) + end + fs(";0;false]") + fs("field[2,3;4,1;carid;Car ID;]") +elseif mem.screenstate == "oobe_connecting" or mem.screenstate == "connecting" then + fs("label[1,1;Connecting to controller...]") +elseif mem.screenstate == "oobe_connectionfailed" or mem.screenstate == "connectionfailed" then + fs("label[4,4;Connection timed out!]") + fs("label[4,5;Make sure the car ID is correct and]") + fs("label[4,5.5;that the controller is ready to pair.]") + fs("button[1,10;2,1;back;< Back]") +end + +mem.infotext = string.format("ID: %d",mem.carid) + +return pos,mem,changedinterrupts diff --git a/init.lua b/init.lua index 855b6a9..7e004c3 100644 --- a/init.lua +++ b/init.lua @@ -8,6 +8,7 @@ local components = { "callbuttons", "pilantern", "fs1switch", + "dispatcher", } for _,v in ipairs(components) do diff --git a/textures/celevator_dispatcher_front_bottom_open.png b/textures/celevator_dispatcher_front_bottom_open.png new file mode 100644 index 0000000000000000000000000000000000000000..67cf132b94ebbc1a062b90e74bf5c1ac35bec0f6 GIT binary patch literal 1794 zcmZuxc{JOJ8vTW|qOB#BwnnLlrI^&xcvEDEWTf*GRN?{vkgO1Udv}7pJu)&Xf*f)^7y$HhlXJ07f4U=X?ORu%j@`bY zOy#+2X83w>UwD_nj@dmMU}to@_zzTQ5vWuI>9C^pagL%a z&l%Ly*E&9>NW>DH_~GI0Vj}B7C$u#5UM@j5ic{DZXl}@;lWA_*M#b%$yfwybUN;e<-6a$1u4R!T!fjjq`=$B6=Jm4jZTcDEH}3*v zzNI6z%d3I!DJ4MK)#;xWx5HclzT|H@;u#tc+4mi zNGG3Y05H=28xUG0?uL|x0k>DMtrcM?@EbUfi4EqMQP~bX97Jl zZJb3l=4nh77Kr>bdj?{MfFIiU|S==S%`BcqkIG$O#5L4^F!A8{Y}!P z%}Dc1yz!&F=spWPSG~;uC}p!ddwP1#Ei#4_cfM_CLIURJYKkA$6N?w!a90eseattPbu}@uc z=ydhG4gPg0ECnhbdM?p2d5b&XHOs9l7ne9a^XF$=;lg2rOV@ z7Bx%1^z8T3QJ@!*=z+L@3Jr9mw|sdufiICWo@!0m+m%QpnOK1_*QpQApu-9sm|eKYH}M^OSSgw87@&t@>Tio! zYE>JhEmo=pl||Iqa{X3u5V_9+X<2(*!>ESM0#Y;CYJoa)>HxRd^}t8-H(t`}z&&c% z^PZc>Rh*|7H@=Z%ftT{B#MtHRR(ed!WOtBEYFFH#lADoi2Zf}dg?MIHlnl4?9iaF`P38PAfQ1F^oEG!6|5&#gnZiTg1= zV5_(*&O@6$PH7OJ5eJ0rH*pBtiB?RDHS+x1Us}aK>INpcSOWlL2lT7&fv@apP<$3AUivS?YsMd*5*eQ|53wB68VWbvpc22zrDi3=KQh$ e?6%#$-ihlPzc3um8u5|-Mu6bp3O=_BNdF((GE%ny literal 0 HcmV?d00001 diff --git a/textures/celevator_dispatcher_front_top.png b/textures/celevator_dispatcher_front_top.png new file mode 100644 index 0000000000000000000000000000000000000000..ffd46a864bd972a9ac3891f012a6ba6fd99fdab1 GIT binary patch literal 7770 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 F{s$R&z5W0I literal 0 HcmV?d00001 diff --git a/textures/celevator_dispatcher_front_top_open.png b/textures/celevator_dispatcher_front_top_open.png new file mode 100644 index 0000000000000000000000000000000000000000..eb6be8f5f44f5ca15486d29dbbcdb679b3217ddd GIT binary patch literal 1539 zcmV+e2K@PnP)EX>4Tx04R}tkvmAkP!xv$KBOWQhjtKg$WR}HT~x%eR-p(LLaorMgUL-_(4-+r zad8w}3l2UOs}3&Cx;nTDg5U$h*}+NCMM^w3DYS_3z~z4Y_xwNio(uS!Ri>K31fXh` zkx3F&H#7uoo6w~mmuY2mGx{LEH@4i2)Un!Uj@Cn2TrW+RV2Jy_M zrE}gVjhen#Jv2O_sX-EP_fP76!7rVXG9WEYbkjoosBK$SMMPC4cURTHV8G0aa?qs&0D8S% zmqw`TvAZwILC(2DV5*daB{30?$KypgP_>B2Ik%;VnKz|oqLk929Efm4v~A1GyUF{XXjYy z2sbNjQB^8M&76rwqtP4(lTmj#o-8jPTc#-?q9g%8>h&r_EUGEZZWTKhc4p$3gP@dB zRh7HD8#PUB?rt72`y4DkS)RH3a}Y#CUDswtj3kmE0vYEgxr@3(a3Ue1h`6a+h`S?U zk~Fi~t$n`s(I?UGukP+bzrT9x7WDhjt#@}1WG-KRcC?#{NXnI|3Ns^OJ#JDpql*6c zdk?;P_PVVirGs#ZncO`fh!G+d){D%$OVr&35ttYf#7P}yvNcWpnj%6VB7i_bK_qi8 zr5tx<&TM&y%$c4L(cPKF!}0$fTd1n4?r0DZBDuSVBO((;1Q8{XhyX-Y9gRjEGw0le z*;VAh&M-Gkb1>Q@jtEnix~Z#t;A4G#eG2T|;m`qA2ZPs2^5*77J#Ivjdq|b4Zo2B` zfJ54U_cs9e>|7rJ0RDZkH{Sm6+w5IK__q>kn& p`}-fv&%rN40KikHx~rau{sZo-W%dQ*zs>*v002ovPDHLkV1oCn%lrTU literal 0 HcmV?d00001 diff --git a/textures/celevator_dispatcher_front_top_open_lside.png b/textures/celevator_dispatcher_front_top_open_lside.png new file mode 100644 index 0000000000000000000000000000000000000000..b2336a6aed9f8a03ae88fe4a3c57066b3119f0ef GIT binary patch literal 8357 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