diff options
author | cheapie <no-email-for-you@example.com> | 2023-08-04 11:25:45 -0500 |
---|---|---|
committer | cheapie <no-email-for-you@example.com> | 2023-08-04 11:25:45 -0500 |
commit | bbdf947d7c1bffb6d26d333c47c9b2a88109f058 (patch) | |
tree | 6a08aa878c06abdf75875d6abf6ddcf5f99011f7 | |
download | celevator-bbdf947d7c1bffb6d26d333c47c9b2a88109f058.tar celevator-bbdf947d7c1bffb6d26d333c47c9b2a88109f058.tar.gz celevator-bbdf947d7c1bffb6d26d333c47c9b2a88109f058.tar.bz2 celevator-bbdf947d7c1bffb6d26d333c47c9b2a88109f058.tar.xz celevator-bbdf947d7c1bffb6d26d333c47c9b2a88109f058.zip |
Add bits that are done so far
This includes:
* Controller (runs and responds to calls placed on the screen, parameter editing and switches work)
* Null Drive (simulates motion so the controller can run, no actual movement yet)
* Call Buttons (lights can be toggled with right-click, no communication yet)
32 files changed, 1607 insertions, 0 deletions
diff --git a/.luacheckrc b/.luacheckrc new file mode 100644 index 0000000..2a41057 --- /dev/null +++ b/.luacheckrc @@ -0,0 +1,12 @@ +max_line_length = 200 + +globals = { + "celevator", +} + +read_globals = { + "DIR_DELIM", + "vector", + "screwdriver", + "minetest", +} @@ -0,0 +1,22 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/callbuttons.lua b/callbuttons.lua new file mode 100644 index 0000000..97d839f --- /dev/null +++ b/callbuttons.lua @@ -0,0 +1,152 @@ +local function makebuttontex(dir,upon,downon) + local tex = "[combine:64x64:0,0=celevator_cabinet_sides.png:32,0=celevator_cabinet_sides.png:0,32=celevator_cabinet_sides.png:32,32=celevator_cabinet_sides.png:22,24=celevator_callbutton_panel.png" + if dir == "up" then + tex = tex..":24,35=celevator_callbutton_up.png" + if upon then + tex = tex..":33,36=celevator_callbutton_light.png" + end + elseif dir == "down" then + tex = tex..":24,35=celevator_callbutton_down.png" + if downon then + tex = tex..":33,36=celevator_callbutton_light.png" + end + elseif dir == "both" then + tex = tex..":24,28=celevator_callbutton_up.png:24,43=celevator_callbutton_down.png" + if upon then + tex = tex..":33,29=celevator_callbutton_light.png" + end + if downon then + tex = tex..":33,44=celevator_callbutton_light.png" + end + end + return(tex) +end + +local validstates = { + {"up",false,false,"Up"}, + {"up",true,false,"Up"}, + {"down",false,false,"Down"}, + {"down",false,true,"Down"}, + {"both",false,false,"Up and Down"}, + {"both",true,false,"Up and Down"}, + {"both",false,true,"Up and Down"}, + {"both",true,true,"Up and Down"}, +} + +local function setlight(pos,dir,newstate) + local node = minetest.get_node(pos) + if minetest.get_item_group(node.name,"_celevator_callbutton") ~= 1 then return end + if dir == "up" then + if minetest.get_item_group(node.name,"_celevator_callbutton_has_up") ~= 1 then return end + local lit = minetest.get_item_group(node.name,"_celevator_callbutton_up_lit") == 1 + if lit == newstate then return end + local newname = "celevator:callbutton_" + if minetest.get_item_group(node.name,"_celevator_callbutton_has_down") == 1 then + newname = newname.."both" + else + newname = newname.."up" + end + if newstate then newname = newname.."_upon" end + if minetest.get_item_group(node.name,"_celevator_callbutton_down_lit") == 1 then + newname = newname.."_downon" + end + node.name = newname + minetest.swap_node(pos,node) + elseif dir == "down" then + if minetest.get_item_group(node.name,"_celevator_callbutton_has_down") ~= 1 then return end + local lit = minetest.get_item_group(node.name,"_celevator_callbutton_down_lit") == 1 + if lit == newstate then return end + local newname = "celevator:callbutton_" + if minetest.get_item_group(node.name,"_celevator_callbutton_has_up") == 1 then + newname = newname.."both" + else + newname = newname.."down" + end + if minetest.get_item_group(node.name,"_celevator_callbutton_up_lit") == 1 then + newname = newname.."_upon" + end + if newstate then newname = newname.."_downon" end + node.name = newname + minetest.swap_node(pos,node) + end +end + +local function disambiguatedir(pos,player) + if player and not player.is_fake_player then + local eyepos = vector.add(player:get_pos(),vector.add(player:get_eye_offset(),vector.new(0,1.5,0))) + local lookdir = player:get_look_dir() + local distance = vector.distance(eyepos,pos) + local endpos = vector.add(eyepos,vector.multiply(lookdir,distance+1)) + local ray = minetest.raycast(eyepos,endpos,true,false) + local pointed,button,hitpos + repeat + pointed = ray:next() + if pointed and pointed.type == "node" then + local node = minetest.get_node(pointed.under) + if node.name and (minetest.get_item_group(node.name,"_celevator_callbutton") == 1) then + button = pointed.under + hitpos = vector.subtract(pointed.intersection_point,button) + end + end + until button or not pointed + if not hitpos then return end + hitpos.y = -1*hitpos.y + hitpos.y = math.floor((hitpos.y+0.5)*64+0.5)+1 + return hitpos.y >= 40 and "down" or "up" + end +end + +for _,state in ipairs(validstates) do + local boringside = "[combine:64x64:0,0=celevator_cabinet_sides.png:32,0=celevator_cabinet_sides.png:0,32=celevator_cabinet_sides.png:32,32=celevator_cabinet_sides.png" + local nname = "celevator:callbutton_"..state[1] + local dropname = nname + if state[2] then nname = nname.."_upon" end + if state[3] then nname = nname.."_downon" end + local idle = not (state[2] or state[3]) + local description = string.format("%s Call Button%s",state[4],(idle and "" or " (on state, you hacker you!)")) + minetest.register_node(nname,{ + description = description, + groups = { + dig_immediate = 2, + not_in_creative_inventory = (idle and 0 or 1), + _celevator_callbutton = 1, + _celevator_callbutton_has_up = (state[1] == "down" and 0 or 1), + _celevator_callbutton_has_down = (state[1] == "up" and 0 or 1), + _celevator_callbutton_up_lit = (state[2] and 1 or 0), + _celevator_callbutton_down_lit = (state[3] and 1 or 0), + }, + drop = dropname, + tiles = { + boringside, + boringside, + boringside, + boringside, + boringside, + makebuttontex(state[1],state[2],state[3]) + }, + paramtype = "light", + paramtype2 = "facedir", + drawtype = "nodebox", + node_box = { + type = "fixed", + fixed = { + {-0.5, -0.5, -0.5, 0.5, 0.5, 0.5 }, + {-0.16, -0.37,-0.59, 0.17, 0.13,-0.5 }, + }, + }, + on_rightclick = function(pos,_,clicker) + if state[1] == "up" then + setlight(pos,"up",not state[2]) + elseif state[1] == "down" then + setlight(pos,"down",not state[3]) + elseif state[1] == "both" then + local dir = disambiguatedir(pos,clicker) + if dir == "up" then + setlight(pos,"up",not state[2]) + elseif dir == "down" then + setlight(pos,"down",not state[3]) + end + end + end, + }) +end diff --git a/controller.lua b/controller.lua new file mode 100644 index 0000000..a2071ec --- /dev/null +++ b/controller.lua @@ -0,0 +1,492 @@ +celevator.controller = {} + +celevator.controller.iqueue = minetest.deserialize(celevator.storage:get_string("controller_iqueue")) or {} + +celevator.controller.equeue = minetest.deserialize(celevator.storage:get_string("controller_equeue")) or {} + +celevator.controller.running = {} + +local fw,err = loadfile(minetest.get_modpath("celevator")..DIR_DELIM.."controllerfw.lua") +if not fw then error(err) end + +minetest.register_chatcommand("celevator_reloadcontroller",{ + params = "", + description = "Reload celevator controller firmware from disk", + privs = {server = true}, + func = function() + local newfw,loaderr = loadfile(minetest.get_modpath("celevator")..DIR_DELIM.."controllerfw.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:controller_top" + minetest.set_node(toppos,node) +end + +local function ondestruct(pos) + pos.y = pos.y + 1 + local topnode = minetest.get_node(pos) + local controllertops = { + ["celevator:controller_top"] = true, + ["celevator:controller_top_running"] = true, + ["celevator:controller_top_open"] = true, + ["celevator:controller_top_open_running"] = true, + } + if controllertops[topnode.name] then + minetest.set_node(pos,{name="air"}) + end + celevator.controller.equeue[minetest.hash_node_position(pos)] = nil + celevator.storage:set_string("controller_equeue",minetest.serialize(celevator.controller.equeue)) +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.controller.run(pos,event) +end + +local function controllerleds(pos,running) + local toppos = vector.add(pos,vector.new(0,1,0)) + local node = minetest.get_node(toppos) + local sparams = { + pos = toppos, + } + if node.name == "celevator:controller_top_open" and running then + node.name = "celevator:controller_top_open_running" + minetest.swap_node(toppos,node) + minetest.sound_play("celevator_controller_start",sparams,true) + elseif node.name == "celevator:controller_top" and running then + node.name = "celevator:controller_top_running" + minetest.swap_node(toppos,node) + minetest.sound_play("celevator_controller_start",sparams,true) + elseif node.name == "celevator:controller_top_open_running" and not running then + node.name = "celevator:controller_top_open" + minetest.swap_node(toppos,node) + minetest.sound_play("celevator_controller_stop",sparams,true) + elseif node.name == "celevator:controller_top_running" and not running then + node.name = "celevator:controller_top" + minetest.swap_node(toppos,node) + minetest.sound_play("celevator_controller_stop",sparams,true) + end +end + +minetest.register_node("celevator:controller",{ + description = "Controller", + 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({})) + local event = {} + event.type = "program" + celevator.controller.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:controller_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) + if node.name == "celevator:controller_top_running" then + node.name = "celevator:controller_top_open_running" + else + node.name = "celevator:controller_top_open" + end + 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:controller_open",{ + description = "Controller (door open - you hacker you!)", + groups = { + cracky = 1, + not_in_creative_inventory = 1, + }, + paramtype = "light", + paramtype2 = "facedir", + drawtype = "nodebox", + drop = "celevator:controller", + 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", + "celevator_cabinet_front_bottom_open.png", + }, + 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:controller" + 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) + if node.name == "celevator:controller_top_open_running" then + node.name = "celevator:controller_top_running" + else + node.name = "celevator:controller_top" + end + 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:controller_top",{ + description = "Controller (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_cabinet_front_top.png", + }, +}) + +minetest.register_node("celevator:controller_top_running",{ + description = "Controller (top section, car in motion - 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_cabinet_front_top.png", + }, +}) + +minetest.register_node("celevator:controller_top_open",{ + description = "Controller (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_cabinet_front_top_open_lside.png", + "celevator_cabinet_sides.png", + { + name="celevator_cabinet_front_top_open_stopped.png", + animation={type="vertical_frames", aspect_w=32, aspect_h=32, length=2}, + } + }, +}) + +minetest.register_node("celevator:controller_top_open_running",{ + description = "Controller (top section, open, car in motion - 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_cabinet_front_top_open_lside.png", + "celevator_cabinet_sides.png", + { + name="celevator_cabinet_front_top_open_running.png", + animation={type="vertical_frames", aspect_w=32, aspect_h=32, length=2}, + } + }, +}) + +function celevator.controller.iscontroller(pos,call2) + local node = minetest.get_node(pos) + if node.name == "ignore" and not call2 then + minetest.forceload_block(pos) + return celevator.controller.iscontroller(pos,true) + elseif node.name == "celevator:controller" or node.name == "celevator:controller_open" then + return true + else + return false + end +end + +function celevator.controller.finddrive(pos) + local node = minetest.get_node(pos) + local dir = minetest.facedir_to_dir(node.param2) + local drivepos = vector.add(pos,vector.new(0,1,0)) + drivepos = vector.add(drivepos,vector.rotate_around_axis(dir,vector.new(0,-1,0),math.pi/2)) + drivepos = vector.round(drivepos) + local drivename = minetest.get_node(drivepos).name + return drivepos,minetest.registered_nodes[drivename]._celevator_drive_type +end + +function celevator.controller.finish(pos,mem) + if not celevator.controller.iscontroller(pos) then + return + else + local drivepos,drivetype = celevator.controller.finddrive(pos) + if drivetype then + for _,command in ipairs(mem.drive.commands) do + if command.command == "moveto" then + celevator.drives[drivetype].moveto(drivepos,command.pos) + elseif command.command == "setmaxvel" then + celevator.drives[drivetype].setmaxvel(drivepos,command.maxvel) + elseif command.command == "resetpos" then + celevator.drives[drivetype].resetpos(drivepos) + elseif command.command == "estop" then + celevator.drives[drivetype].estop(drivepos) + end + end + end + local meta = minetest.get_meta(pos) + local node = minetest.get_node(pos) + 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 "") + meta:set_string("infotext",mem.infotext or "") + local hash = minetest.hash_node_position(pos) + celevator.controller.iqueue[hash] = mem.interrupts + celevator.storage:set_string("controller_iqueue",minetest.serialize(celevator.controller.iqueue)) + controllerleds(pos,mem.showrunning) + celevator.controller.running[hash] = nil + if #celevator.controller.equeue[hash] > 0 then + local event = celevator.controller.equeue[hash][1] + table.remove(celevator.controller.equeue[hash],1) + celevator.storage:set_string("controller_equeue",minetest.serialize(celevator.controller.equeue)) + celevator.controller.run(pos,event) + end + end +end + +function celevator.controller.run(pos,event) + if not celevator.controller.iscontroller(pos) then + return + else + local hash = minetest.hash_node_position(pos) + if not celevator.controller.equeue[hash] then + celevator.controller.equeue[hash] = {} + celevator.storage:set_string("controller_equeue",minetest.serialize(celevator.controller.equeue)) + end + if celevator.controller.running[hash] then + table.insert(celevator.controller.equeue[hash],event) + celevator.storage:set_string("controller_equeue",minetest.serialize(celevator.controller.equeue)) + if #celevator.controller.equeue[hash] > 5 then + minetest.log("warning","[celevator] [controller] Async process for controller at %s is falling behind, %d events in queue",minetest.pos_to_string(pos),#celevator.controller.equeue[hash]) + end + return + end + celevator.controller.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 controller memory at "..minetest.pos_to_string(pos)) + mem = {} + end + mem.drive = {} + mem.drive.commands = {} + local drivepos,drivetype = celevator.controller.finddrive(pos) + if drivetype then + mem.drive.type = drivetype + mem.drive.status = celevator.drives[drivetype].getstatus(drivepos) + end + mem.interrupts = celevator.controller.iqueue[minetest.hash_node_position(pos)] or {} + minetest.handle_async(fw,celevator.controller.finish,pos,event,mem) + end +end + +function celevator.controller.checkiqueue(dtime) + for hash,iqueue in pairs(celevator.controller.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.controller.run(pos,event) + end + end + end +end + +minetest.register_globalstep(celevator.controller.checkiqueue) diff --git a/controllerfw.lua b/controllerfw.lua new file mode 100644 index 0000000..e21a9e7 --- /dev/null +++ b/controllerfw.lua @@ -0,0 +1,707 @@ +local pos,event,mem = ... + +local function fault(ftype,fatal) + if fatal then mem.fatalfault = true end + if not mem.activefaults then mem.activefaults = {} end + if not mem.faultlog then mem.faultlog = {} end + if mem.activefaults[ftype] then return end + mem.activefaults[ftype] = true + table.insert(mem.faultlog,{ftype = ftype,timestamp = os.time()}) +end + +if not mem.drive.status then + fault("drivecomm",true) + mem.drive.status = { + apos = 0, + dpos = 0, + vel = 0, + maxvel = 0, + } +end + +local juststarted = false + +local modenames = { + normal = "Normal Operation", + uninit = "Uninitialized", + resync = "Position Sync - Floor", + bfdemand = "Position Sync - Terminal", + fault = "Fault", + stop = "Emergency Stop", + mrinspect = "Machine Room Inspection", + carinspect = "Car Top Inspection", + inspconflict = "Inspection Conflict", + fs1 = "Fire Service - Phase 1", + fs2 = "Fire Service - Phase 2", + indep = "Independent Service", + capture = "Captured", + test = "Test Mode", +} + +local doorstates = { + open = "Open", + opening = "Opening", + closing = "Closing", + closed = "Closed", + testtiming = "Closed", +} + +local faultnames = { + drivecomm = "Lost Communication With Drive", +} + +local function drivecmd(command) + table.insert(mem.drive.commands,command) +end + +local function interrupt(time,iid) + mem.interrupts[iid] = time +end + +local function getpos() + local ret = 0 + for k,v in ipairs(mem.params.floorheights) do + ret = ret+v + if ret > mem.drive.status.apos then return k end + end + return mem.params.floorheights[#mem.params.floorheights] +end + +local function gettarget(floor) + local target = 0 + if floor == 1 then return 0 end + for i=1,floor-1,1 do + target = target+mem.params.floorheights[i] + end + return target +end + +local function gotofloor(floor) + mem.carmotion = true + drivecmd({ + command = "setmaxvel", + maxvel = mem.params.contractspeed + }) + drivecmd({ + command = "moveto", + pos = gettarget(floor) + }) + interrupt(0,"checkdrive") + juststarted = true +end + +local function getnextcallabove(dir) + for i=getpos(),#mem.params.floorheights,1 do + if not dir then + if mem.carcalls[i] then + return i,"car" + elseif mem.upcalls[i] then + return i,"up" + elseif mem.dncalls[i] then + return i,"down" + end + elseif dir == "up" then + if mem.carcalls[i] then + return i,"car" + elseif mem.upcalls[i] then + return i,"up" + end + elseif dir == "down" then + if mem.carcalls[i] then + return i,"car" + elseif mem.dncalls[i] then + return i,"down" + end + end + end +end + +local function getnextcallbelow(dir) + for i=getpos(),1,-1 do + if not dir then + if mem.carcalls[i] then + return i,"car" + elseif mem.upcalls[i] then + return i,"up" + elseif mem.dncalls[i] then + return i,"down" + end + elseif dir == "up" then + if mem.carcalls[i] then + return i,"car" + elseif mem.upcalls[i] then + return i,"up" + end + elseif dir == "down" then + if mem.carcalls[i] then + return i,"car" + elseif mem.dncalls[i] then + return i,"down" + end + end + end +end + +local function getlowestupcall() + for i=1,#mem.params.floornames,1 do + if mem.upcalls[i] then return i end + end +end + +local function gethighestdowncall() + for i=#mem.params.floornames,1,-1 do + if mem.dncalls[i] then return i end + end +end + +local function open() + --TODO: Door operator interface + mem.doorstate = "opening" + interrupt(2,"opened") +end + +local function close() + --TODO: Door operator interface + mem.doorstate = "closing" + interrupt(2,"closed") +end + +mem.formspec = "" + +local function fs(element) + mem.formspec = mem.formspec..element +end + +if event.type == "program" then + mem.carstate = "uninit" + mem.editingfloor = 1 + mem.doorstate = "closed" + mem.carmotion = false + mem.carcalls = {} + mem.upcalls = {} + mem.dncalls = {} + mem.screenpage = 1 + mem.scrollfollowscar = true + mem.controllerstopsw = false + mem.controllerinspectsw = false + mem.cartopinspectsw = false + mem.capturesw = false + mem.testsw = false + mem.activefaults = {} + mem.faultlog = {} + mem.fatalfault = false + if not mem.params then + mem.state = "unconfigured" + mem.screenstate = "oobe_welcome" + mem.params = { + contractspeed = 1, + floorheights = {5,5,5}, + floornames = {"1","2","3"}, + doortimer = 5, + groupmode = "simplex", + } + end +elseif event.type == "ui" then + if mem.screenstate == "oobe_welcome" then + if event.fields.license then + mem.screenstate = "oobe_license" + elseif event.fields.next then + mem.screenstate = "oobe_groupmode" + end + elseif mem.screenstate == "oobe_license" then + if event.fields.back then + mem.screenstate = "oobe_welcome" + end + elseif mem.screenstate == "oobe_groupmode" then + if event.fields.back then + mem.screenstate = "oobe_welcome" + elseif event.fields.simplex then + mem.screenstate = "oobe_floortable" + mem.params.groupmode = "simplex" + elseif event.fields.group then + mem.screenstate = "oobe_dispatcherconnect" + mem.params.groupmode = "group" + end + elseif mem.screenstate == "oobe_dispatcherconnect" then + if event.fields.back then + mem.screenstate = "oobe_groupmode" + 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_groupmode" + elseif event.fields.next then + if mem.screenstate == "oobe_floortable" then + mem.activefaults = {} + mem.faultlog = {} + mem.fatalfault = false + end + mem.state = "configured" + mem.screenstate = (mem.screenstate == "oobe_floortable" and "status" or "parameters") + 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 + 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 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 == "parameters" then + if event.fields.save then + mem.screenstate = "status" + local doortimer = tonumber(event.fields.doortimer) + if doortimer and doortimer > 0 and doortimer <= 30 then + mem.params.doortimer = doortimer + end + local contractspeed = tonumber(event.fields.contractspeed) + if contractspeed and contractspeed >= 0.1 and contractspeed <= 20 then + mem.params.contractspeed = contractspeed + end + elseif event.fields.floortable then + mem.screenstate = "floortable" + elseif event.fields.cancel then + mem.screenstate = "status" + end + elseif mem.screenstate == "status" then + for i=1,#mem.params.floornames,1 do + if event.fields[string.format("carcall%d",i)] and (mem.carstate == "normal" or mem.carstate == "test" or mem.carstate == "capture") then + mem.carcalls[i] = true + elseif event.fields[string.format("upcall%d",i)] and mem.carstate == "normal" and not mem.capturesw then + mem.upcalls[i] = true + elseif event.fields[string.format("downcall%d",i)] and mem.carstate == "normal" and not mem.capturesw then + mem.dncalls[i] = true + end + end + if event.fields.scrollup then + mem.screenpage = mem.screenpage + 1 + mem.scrollfollowscar = false + elseif event.fields.scrolldown then + mem.screenpage = mem.screenpage - 1 + mem.scrollfollowscar = false + elseif event.fields.scrollfollowscar then + mem.scrollfollowscar = (event.fields.scrollfollowscar == "true") + elseif event.fields.stopsw then + mem.controllerstopsw = not mem.controllerstopsw + elseif event.fields.inspectsw then + mem.controllerinspectsw = not mem.controllerinspectsw + elseif event.fields.capturesw then + mem.capturesw = not mem.capturesw + elseif event.fields.testsw then + mem.testsw = not mem.testsw + elseif event.fields.inspectup and mem.carstate == "mrinspect" and mem.doorstate == "closed" and getpos() < #mem.params.floornames then + mem.carmotion = true + juststarted = true + drivecmd({ + command = "setmaxvel", + maxvel = 0.2, + }) + drivecmd({ + command = "moveto", + pos = math.floor(mem.drive.status.apos)+1 + }) + elseif event.fields.inspectdown and mem.carstate == "mrinspect" and mem.doorstate == "closed" and mem.drive.status.apos-1 >= 0 then + mem.carmotion = true + juststarted = true + drivecmd({ + command = "setmaxvel", + maxvel = 0.2, + }) + drivecmd({ + command = "moveto", + pos = math.floor(mem.drive.status.apos)-1 + }) + elseif event.fields.parameters then + mem.screenstate = "parameters" + elseif event.fields.faults then + mem.screenstate = "faults" + end + elseif mem.screenstate == "faults" then + if event.fields.back then + mem.screenstate = "status" + elseif event.fields.clear then + mem.faultlog = {} + mem.activefaults = {} + mem.fatalfault = false + end + end +elseif event.iid == "opened" and mem.doorstate == "opening" then + mem.doorstate = "open" + if mem.carstate == "normal" then + interrupt(mem.params.doortimer,"close") + end +elseif event.iid == "close" and mem.doorstate == "open" then + close() +elseif event.iid == "closed" and (mem.doorstate == "closing" or mem.doorstate == "testtiming") then + mem.doorstate = "closed" + if mem.carstate == "bfdemand" then + drivecmd({ + command = "setmaxvel", + maxvel = mem.params.contractspeed, + }) + drivecmd({command = "resetpos"}) + interrupt(0.1,"checkdrive") + mem.carmotion = true + juststarted = true + elseif mem.carstate == "resync" then + gotofloor(getpos()) + interrupt(0.1,"checkdrive") + mem.carmotion = true + juststarted = true + end +end + +local oldstate = mem.carstate + +if mem.fatalfault then + mem.carstate = "fault" + drivecmd({command="estop"}) + mem.carcalls = {} + mem.upcalls = {} + mem.dncalls = {} + mem.direction = nil +elseif mem.controllerstopsw or mem.screenstate == "floortable" or mem.screenstate == "floortable_edit" then + mem.carstate = "stop" + drivecmd({command="estop"}) + mem.carcalls = {} + mem.upcalls = {} + mem.dncalls = {} + mem.direction = nil +elseif mem.controllerinspectsw and not mem.cartopinspectsw then + mem.carstate = "mrinspect" + mem.carcalls = {} + mem.upcalls = {} + mem.dncalls = {} + mem.direction = nil + if oldstate ~= "mrinspect" then drivecmd({command="estop"}) end +elseif mem.testsw then + mem.upcalls = {} + mem.dncalls = {} + mem.carstate = "test" +elseif mem.capturesw then + mem.upcalls = {} + mem.dncalls = {} + if not mem.direction then mem.carstate = "capture" end +else + if oldstate == "stop" or oldstate == "mrinspect" or oldstate == "fault" then + mem.carstate = "resync" + gotofloor(getpos()) + elseif oldstate == "test" or oldstate == "capture" then + mem.carstate = "normal" + end +end + +if mem.carmotion then + mem.carmotion = (mem.drive.status.vel ~= 0) or juststarted + if mem.carmotion then + interrupt(0.1,"checkdrive") + else + if mem.carstate == "normal" then + mem.carcalls[getpos()] = nil + if mem.direction == "up" then + mem.upcalls[getpos()] = nil + elseif mem.direction == "down" then + mem.dncalls[getpos()] = nil + end + if getpos() >= #mem.params.floornames then + mem.direction = "down" + elseif getpos() <= 1 then + mem.direction = "up" + end + open() + elseif mem.carstate == "test" then + mem.carcalls[getpos()] = nil + mem.doorstate = "testtiming" + interrupt(5,"closed") + if getpos() >= #mem.params.floornames then + mem.direction = "down" + elseif getpos() <= 1 then + mem.direction = "up" + end + elseif mem.carstate == "bfdemand" or mem.carstate == "resync" then + mem.carstate = "normal" + end + end +end + +if (mem.carstate == "normal" or mem.carstate == "capture" or mem.carstate == "test") and mem.doorstate == "closed" and not mem.carmotion then + if mem.direction == "up" then + if getnextcallabove("up") then + mem.direction = "up" + gotofloor(getnextcallabove("up")) + elseif gethighestdowncall() then + mem.direction = "down" + gotofloor(gethighestdowncall()) + elseif getlowestupcall() then + gotofloor(getlowestupcall()) + elseif getnextcallbelow("down") then + mem.direction = "down" + gotofloor(getnextcallbelow("down")) + else + mem.direction = nil + end + elseif mem.direction == "down" then + if getnextcallbelow("down") then + gotofloor(getnextcallbelow("down")) + elseif getlowestupcall() then + mem.direction = "up" + gotofloor(getlowestupcall()) + elseif gethighestdowncall() then + gotofloor(gethighestdowncall()) + elseif getnextcallabove("up") then + mem.direction = "up" + gotofloor(getnextcallabove()) + else + mem.direction = nil + end + else + if getnextcallabove("up") then + mem.direction = "up" + gotofloor(getnextcallabove()) + elseif getnextcallbelow("down") then + mem.direction = "down" + gotofloor(getnextcallbelow("down")) + elseif getlowestupcall() then + mem.direction = "up" + gotofloor(getlowestupcall()) + elseif gethighestdowncall() then + mem.direction = "down" + gotofloor(gethighestdowncall()) + end + end + if mem.carstate == "normal" and mem.capturesw and not mem.direction then + mem.upcalls = {} + mem.dncalls = {} + mem.carstate = "capture" + elseif mem.carstate == "capture" and mem.direction then + mem.carstate = "normal" + end +end + +if mem.scrollfollowscar and mem.screenstate == "status" then + mem.screenpage = math.floor((getpos()-1)/10)+1 +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 controller!]") + fs("label[1,4.5;This setup wizard is designed to get your elevator up and running as quickly as possible.]") + 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 controller:;"..license.."]") + fs("button[7,10.5;2,1;back;OK]") +elseif mem.screenstate == "oobe_groupmode" then + fs("button[1,10;2,1;back;< Back]") + fs("label[1,1;Select a group operation mode:]") + 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)]") +elseif mem.screenstate == "oobe_dispatcherconnect" then + fs("button[1,10;2,1;back;< Back]") + fs("label[1,1;Not yet implemented. Press Back.]") +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.]") + fs("button[1,10;2,1;back;< Back]") + fs("button[13,10;2,1;next;Done >]") + 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]") + fs("button[8,2;2,1;add;New Floor]") + 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 == "status" then + fs("style_type[image_button;font=mono;font_size=*0.75]") + fs("box[12,2.5;0.1,9;#AAAAAAFF]") + fs("box[13.12,2.5;0.05,9;#AAAAAAFF]") + fs("box[14.12,2.5;0.05,9;#AAAAAAFF]") + fs("box[15.25,2.5;0.1,9;#AAAAAAFF]") + fs("label[12.5,2;UP]") + fs("label[13.38,2;CAR]") + fs("label[14.25,2;DOWN]") + local maxfloor = #mem.params.floornames + local bottom = (mem.screenpage-1)*10+1 + for i=0,9,1 do + local ypos = 11-(i*0.9) + local floornum = bottom+i + if floornum > maxfloor then break end + fs(string.format("label[11.25,%f;%s]",ypos,mem.params.floornames[floornum])) + local ccdot = mem.carcalls[floornum] and "*" or "" + if getpos() == floornum then + local cargraphics = { + open = "\\[ \\]", + opening = "\\[< >\\]", + closing = "\\[> <\\]", + closed = "\\[ | \\]", + testtiming = "\\[ | \\]", + } + ccdot = cargraphics[mem.doorstate] + if mem.direction == "up" then + ccdot = minetest.colorize("#55FF55",ccdot) + elseif mem.direction == "down" then + ccdot = minetest.colorize("#FF5555",ccdot) + end + end + fs(string.format("image_button[13.25,%f;0.75,0.75;celevator_fs_bg.png;carcall%d;%s]",ypos-0.25,floornum,ccdot)) + if floornum < maxfloor then + local arrow = mem.upcalls[floornum] and minetest.colorize("#55FF55","^") or "" + fs(string.format("image_button[12.25,%f;0.75,0.75;celevator_fs_bg.png;upcall%d;%s]",ypos-0.25,floornum,arrow)) + end + if floornum > 1 then + local arrow = mem.dncalls[floornum] and minetest.colorize("#FF5555","v") or "" + fs(string.format("image_button[14.25,%f;0.75,0.75;celevator_fs_bg.png;downcall%d;%s]",ypos-0.25,floornum,arrow)) + end + end + if maxfloor > 10 then + fs(string.format("checkbox[13,1.25;scrollfollowscar;Follow Car;%s]",tostring(mem.scrollfollowscar))) + if bottom+9 < maxfloor then + fs("image_button[12.75,0.25;0.75,0.75;celevator_menu_arrow.png;scrollup;;false;false;celevator_menu_arrow.png]") + end + if bottom > 1 then + fs("image_button[13.87,0.25;0.75,0.75;celevator_menu_arrow.png^\\[transformFY;scrolldown;;false;false;celevator_menu_arrow.png^\\[transformFY]") + end + end + fs("label[1,1;CAR STATUS]") + fs(string.format("label[1,2;%s]",modenames[mem.carstate])) + fs(string.format("label[1,2.5;Doors %s]",doorstates[mem.doorstate])) + fs(string.format("label[1,3;Position: %0.02fm Speed: %+0.02fm/s PI: %s]",mem.drive.status.apos,mem.drive.status.vel,minetest.formspec_escape(mem.params.floornames[getpos()]))) + if #mem.faultlog > 0 then + fs("label[1,3.5;Fault(s) Active]") + else + fs("label[1,3.5;No Current Faults]") + end + fs("button[1,10;3,1;faults;Fault History]") + fs("button[4.5,10;3,1;parameters;Edit Parameters]") + fs("style[*;font=mono]") + local stopswimg = "celevator_toggle_switch.png"..(mem.controllerstopsw and "^\\[transformFY" or "") + fs(string.format("image_button[1,5;1,1.33;%s;stopsw;;false;false;%s]",stopswimg,stopswimg)) + fs("label[1.3,4.75;RUN]") + fs("label[1.2,6.6;STOP]") + local captureswimg = "celevator_toggle_switch.png"..(mem.capturesw and "" or "^\\[transformFY") + fs(string.format("image_button[3,5;1,1.33;%s;capturesw;;false;false;%s]",captureswimg,captureswimg)) + fs("label[3,4.75;CAPTURE]") + local testswimg = "celevator_toggle_switch.png"..(mem.testsw and "" or "^\\[transformFY") + fs(string.format("image_button[5,5;1,1.33;%s;testsw;;false;false;%s]",testswimg,testswimg)) + fs("label[5.23,4.75;TEST]") + local inspectswimg = "celevator_toggle_switch.png"..(mem.controllerinspectsw and "" or "^\\[transformFY") + fs(string.format("image_button[1,8;1,1.33;%s;inspectsw;;false;false;%s]",inspectswimg,inspectswimg)) + fs("label[1.05,7.75;INSPECT]") + fs("label[1.1,9.6;NORMAL]") + fs(string.format("image_button[3,8.25;1,1;%s;inspectup;;false;false;%s]","celevator_button_black.png","celevator_button_black.png")) + fs("label[3.4,7.75;UP]") + fs(string.format("image_button[5,8.25;1,1;%s;inspectdown;;false;false;%s]","celevator_button_black.png","celevator_button_black.png")) + fs("label[5.25,7.75;DOWN]") +elseif mem.screenstate == "parameters" then + fs("label[1,1;EDIT PARAMETERS]") + fs("button[1,10;3,1;save;Save]") + fs("button[4.5,10;3,1;cancel;Cancel]") + fs("button[8,10;3,1;floortable;Edit Floor Table]") + fs(string.format("field[1,3;3,1;doortimer;Door Dwell Timer;%0.1f]",mem.params.doortimer)) + fs(string.format("field[1,5;3,1;contractspeed;Contract Speed (m/s);%0.1f]",mem.params.contractspeed)) +elseif mem.screenstate == "faults" then + fs("label[1,1;FAULT HISTORY]") + if #mem.faultlog > 0 then + for i=0,9,1 do + if #mem.faultlog-i >= 1 then + local currfault = mem.faultlog[#mem.faultlog-i] + local date = os.date("*t",currfault.timestamp) + fs(string.format("label[1,%0.1f;%04d-%02d-%02d %02d:%02d:%02d - %s]",2+i,date.year,date.month,date.day,date.hour,date.min,date.sec,faultnames[currfault.ftype])) + end + end + else + fs("label[1,2;No Current Faults]") + end + fs("button[1,10;3,1;back;Back]") + fs("button[4.5,10;3,1;clear;Clear]") +end + +local arrow = " " +if mem.drive.status.dpos > mem.drive.status.apos then + arrow = "^" +elseif mem.drive.status.dpos < mem.drive.status.apos then + arrow = "v" +end +mem.infotext = string.format("Floor %s %s - %s - Doors %s",mem.params.floornames[getpos()],arrow,modenames[mem.carstate],doorstates[mem.doorstate]) + +if mem.drive.type then + mem.showrunning = mem.drive.status.vel ~= 0 +else + mem.showrunning = false +end + +return pos,mem diff --git a/drive_null.lua b/drive_null.lua new file mode 100644 index 0000000..17ed649 --- /dev/null +++ b/drive_null.lua @@ -0,0 +1,206 @@ +celevator.drives.null = { + name = "Null Drive", + description = "Simulation only, no movement, for testing and demonstration", + nname = "celevator:drive_null", + soundhandles = {}, +} + +local function update_ui(pos) + local meta = minetest.get_meta(pos) + local apos = tonumber(meta:get_string("apos")) or 0 + local status = "Idle" + local vel = tonumber(meta:get_string("vel")) or 0 + if vel > 0 then + status = string.format("Running: Up, %0.02f m/s",vel) + elseif vel < 0 then + status = string.format("Running: Down, %0.02f m/s",math.abs(vel)) + end + meta:set_string("infotext",string.format("Null Drive - %s - Position: %0.02f m",status,apos)) +end + +local function playbuzz(pos) + local hash = minetest.hash_node_position(pos) + if celevator.drives.null.soundhandles[hash] == "cancel" then return end + celevator.drives.null.soundhandles[hash] = minetest.sound_play("celevator_drive_run",{ + pos = pos, + loop = true, + gain = 0.4, + }) +end + +local function startbuzz(pos) + local hash = minetest.hash_node_position(pos) + if celevator.drives.null.soundhandles[hash] == "cancel" then + celevator.drives.null.soundhandles[hash] = nil + return + end + if celevator.drives.null.soundhandles[hash] then return end + celevator.drives.null.soundhandles[hash] = "pending" + minetest.after(0.5,playbuzz,pos) +end + +local function stopbuzz(pos) + local hash = minetest.hash_node_position(pos) + if not celevator.drives.null.soundhandles[hash] then return end + if celevator.drives.null.soundhandles[hash] == "pending" then + celevator.drives.null.soundhandles[hash] = "cancel" + end + if type(celevator.drives.null.soundhandles[hash]) ~= "string" then + minetest.sound_stop(celevator.drives.null.soundhandles[hash]) + celevator.drives.null.soundhandles[hash] = nil + end +end + +minetest.register_node("celevator:drive_null",{ + description = celevator.drives.null.name, + groups = { + cracky = 1, + }, + tiles = { + "celevator_cabinet_sides.png", + "celevator_cabinet_sides.png", + "celevator_cabinet_sides.png", + "celevator_cabinet_sides.png", + "celevator_cabinet_sides.png", + "celevator_drive_front.png", + }, + paramtype = "light", + paramtype2 = "facedir", + drawtype = "nodebox", + node_box = { + type = "fixed", + fixed = { + {-0.4,-0.4,-0.1,0.4,0.4,0.5}, + {-0.5,-0.3,0.4,-0.4,-0.22,0.32}, + {-0.5,0.22,0.4,-0.4,0.3,0.32}, + }, + }, + _celevator_drive_type = "null", + after_place_node = function(pos) + local meta = minetest.get_meta(pos) + meta:set_string("apos","0") + meta:set_string("dpos","0") + meta:set_string("vel","0") + meta:set_string("maxvel","0.2") + update_ui(pos) + end, + on_destruct = stopbuzz, +}) + +function celevator.drives.null.step(dtime) + local nulldrives_running = minetest.deserialize(celevator.storage:get_string("nulldrives_running")) or {} + local save = false + for i,hash in ipairs(nulldrives_running) do + save = true + local pos = minetest.get_position_from_hash(hash) + local node = minetest.get_node(pos) + local sound = false + if node.name == "ignore" then + minetest.forceload_block(pos,true) + elseif node.name ~= "celevator:drive_null" then + table.remove(nulldrives_running,i) + else + local meta = minetest.get_meta(pos) + local apos = tonumber(meta:get_string("apos")) or 0 + local dpos = tonumber(meta:get_string("dpos")) or 0 + local maxvel = tonumber(meta:get_string("maxvel")) or 0.2 + local dremain = math.abs(dpos-apos) + local vel = maxvel + if dremain < 0.5 then vel = math.min(0.2,vel) end + local stepdist = vel*dtime + if dpos > apos then + local newpos = apos + stepdist + if newpos < dpos then + meta:set_string("apos",tostring(newpos)) + meta:set_string("vel",vel) + sound = true + else + meta:set_string("apos",tostring(dpos)) + meta:set_string("vel",0) + sound = false + end + elseif dpos < apos then + local newpos = apos - stepdist + if newpos > dpos then + meta:set_string("apos",tostring(newpos)) + meta:set_string("vel",0-vel) + sound = true + else + meta:set_string("apos",tostring(dpos)) + meta:set_string("vel",0) + sound = false + end + else + table.remove(nulldrives_running,i) + end + end + update_ui(pos) + if sound then + startbuzz(pos) + else + stopbuzz(pos) + end + end + if save then + celevator.storage:set_string("nulldrives_running",minetest.serialize(nulldrives_running)) + end +end + +minetest.register_globalstep(celevator.drives.null.step) + +function celevator.drives.null.moveto(pos,target) + local meta = minetest.get_meta(pos) + meta:set_string("dpos",tostring(target)) + local hash = minetest.hash_node_position(pos) + local nulldrives_running = minetest.deserialize(celevator.storage:get_string("nulldrives_running")) or {} + local running = false + for _,dhash in ipairs(nulldrives_running) do + if hash == dhash then + running = true + break + end + end + if not running then + table.insert(nulldrives_running,hash) + celevator.storage:set_string("nulldrives_running",minetest.serialize(nulldrives_running)) + end +end + +function celevator.drives.null.resetpos(pos) + celevator.drives.null.moveto(pos,0) +end + +function celevator.drives.null.estop(pos) + local meta = minetest.get_meta(pos) + meta:set_string("dpos",meta:get_string("apos")) + meta:set_string("vel","0") +end + +function celevator.drives.null.setmaxvel(pos,maxvel) + local meta = minetest.get_meta(pos) + meta:set_string("maxvel",tostring(maxvel)) +end + +function celevator.drives.null.rezero(pos) + celevator.drives.null.moveto(pos,0) +end + +function celevator.drives.null.getstatus(pos,call2) + local node = minetest.get_node(pos) + if node.name == "ignore" and not call2 then + minetest.forceload_block(pos,true) + return celevator.drives.null.get_status(pos,true) + elseif node.name ~= "celevator:drive_null" then + minetest.log("error","[celevator] [null drive] Could not load drive status at "..minetest.pos_to_string(pos)) + return + else + local meta = minetest.get_meta(pos) + local ret = {} + ret.apos = tonumber(meta:get_string("apos")) or 0 + ret.dpos = tonumber(meta:get_string("dpos")) or 0 + ret.vel = tonumber(meta:get_string("vel")) or 0 + ret.maxvel = tonumber(meta:get_string("maxvel")) or 0.2 + ret.neareststop = ret.apos + return ret + end +end diff --git a/framework.lua b/framework.lua new file mode 100644 index 0000000..695ac17 --- /dev/null +++ b/framework.lua @@ -0,0 +1,4 @@ +celevator = { + drives = {}, + storage = minetest.get_mod_storage(), +} diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..0c44a32 --- /dev/null +++ b/init.lua @@ -0,0 +1,10 @@ +local components = { + "framework", + "drive_null", + "controller", + "callbuttons", +} + +for _,v in ipairs(components) do + dofile(string.format("%s%s%s.lua",minetest.get_modpath("celevator"),DIR_DELIM,v)) +end diff --git a/mod.conf b/mod.conf new file mode 100644 index 0000000..1daf236 --- /dev/null +++ b/mod.conf @@ -0,0 +1,2 @@ +name = celevator +description = WIP diff --git a/sounds/celevator_controller_start.ogg b/sounds/celevator_controller_start.ogg Binary files differnew file mode 100644 index 0000000..e370621 --- /dev/null +++ b/sounds/celevator_controller_start.ogg diff --git a/sounds/celevator_controller_stop.ogg b/sounds/celevator_controller_stop.ogg Binary files differnew file mode 100644 index 0000000..a59a2f6 --- /dev/null +++ b/sounds/celevator_controller_stop.ogg diff --git a/sounds/celevator_drive_run.ogg b/sounds/celevator_drive_run.ogg Binary files differnew file mode 100644 index 0000000..d9ec0d6 --- /dev/null +++ b/sounds/celevator_drive_run.ogg diff --git a/textures/celevator_button_black.png b/textures/celevator_button_black.png Binary files differnew file mode 100644 index 0000000..99de0ba --- /dev/null +++ b/textures/celevator_button_black.png diff --git a/textures/celevator_cabinet_front_bottom.png b/textures/celevator_cabinet_front_bottom.png Binary files differnew file mode 100644 index 0000000..89422a9 --- /dev/null +++ b/textures/celevator_cabinet_front_bottom.png diff --git a/textures/celevator_cabinet_front_bottom_open.png b/textures/celevator_cabinet_front_bottom_open.png Binary files differnew file mode 100644 index 0000000..b5abced --- /dev/null +++ b/textures/celevator_cabinet_front_bottom_open.png diff --git a/textures/celevator_cabinet_front_bottom_open_lside.png b/textures/celevator_cabinet_front_bottom_open_lside.png Binary files differnew file mode 100644 index 0000000..1afbcaf --- /dev/null +++ b/textures/celevator_cabinet_front_bottom_open_lside.png diff --git a/textures/celevator_cabinet_front_bottom_open_rside.png b/textures/celevator_cabinet_front_bottom_open_rside.png Binary files differnew file mode 100644 index 0000000..d0ffaaf --- /dev/null +++ b/textures/celevator_cabinet_front_bottom_open_rside.png diff --git a/textures/celevator_cabinet_front_top.png b/textures/celevator_cabinet_front_top.png Binary files differnew file mode 100644 index 0000000..8e97201 --- /dev/null +++ b/textures/celevator_cabinet_front_top.png diff --git a/textures/celevator_cabinet_front_top_open_lside.png b/textures/celevator_cabinet_front_top_open_lside.png Binary files differnew file mode 100644 index 0000000..a962875 --- /dev/null +++ b/textures/celevator_cabinet_front_top_open_lside.png diff --git a/textures/celevator_cabinet_front_top_open_rside.png b/textures/celevator_cabinet_front_top_open_rside.png Binary files differnew file mode 100644 index 0000000..1aa2811 --- /dev/null +++ b/textures/celevator_cabinet_front_top_open_rside.png diff --git a/textures/celevator_cabinet_front_top_open_running.png b/textures/celevator_cabinet_front_top_open_running.png Binary files differnew file mode 100644 index 0000000..d1e25d6 --- /dev/null +++ b/textures/celevator_cabinet_front_top_open_running.png diff --git a/textures/celevator_cabinet_front_top_open_stopped.png b/textures/celevator_cabinet_front_top_open_stopped.png Binary files differnew file mode 100644 index 0000000..22bb896 --- /dev/null +++ b/textures/celevator_cabinet_front_top_open_stopped.png diff --git a/textures/celevator_cabinet_sides.png b/textures/celevator_cabinet_sides.png Binary files differnew file mode 100644 index 0000000..bc3cebd --- /dev/null +++ b/textures/celevator_cabinet_sides.png diff --git a/textures/celevator_callbutton_down.png b/textures/celevator_callbutton_down.png Binary files differnew file mode 100644 index 0000000..14bb89e --- /dev/null +++ b/textures/celevator_callbutton_down.png diff --git a/textures/celevator_callbutton_light.png b/textures/celevator_callbutton_light.png Binary files differnew file mode 100644 index 0000000..eb36837 --- /dev/null +++ b/textures/celevator_callbutton_light.png diff --git a/textures/celevator_callbutton_panel.png b/textures/celevator_callbutton_panel.png Binary files differnew file mode 100644 index 0000000..018a32b --- /dev/null +++ b/textures/celevator_callbutton_panel.png diff --git a/textures/celevator_callbutton_up.png b/textures/celevator_callbutton_up.png Binary files differnew file mode 100644 index 0000000..29176ed --- /dev/null +++ b/textures/celevator_callbutton_up.png diff --git a/textures/celevator_drive_front.png b/textures/celevator_drive_front.png Binary files differnew file mode 100644 index 0000000..6331c6a --- /dev/null +++ b/textures/celevator_drive_front.png diff --git a/textures/celevator_fs_bg.png b/textures/celevator_fs_bg.png Binary files differnew file mode 100644 index 0000000..87c54f2 --- /dev/null +++ b/textures/celevator_fs_bg.png diff --git a/textures/celevator_logo.png b/textures/celevator_logo.png Binary files differnew file mode 100644 index 0000000..faa4084 --- /dev/null +++ b/textures/celevator_logo.png diff --git a/textures/celevator_menu_arrow.png b/textures/celevator_menu_arrow.png Binary files differnew file mode 100644 index 0000000..3cf735b --- /dev/null +++ b/textures/celevator_menu_arrow.png diff --git a/textures/celevator_toggle_switch.png b/textures/celevator_toggle_switch.png Binary files differnew file mode 100644 index 0000000..b2df862 --- /dev/null +++ b/textures/celevator_toggle_switch.png |