# Joystick configuration library. var DIALOGROOT = "/sim/gui/dialogs/joystick-config"; var MAX_AXES = 8; var MAX_NASALS = 8; var MAX_BUTTONS = 24; # Hash of the custom axis/buttons var custom_bindings = {}; # Class for an individual joystick axis binding var Axis = { new: func(name, prop, invertable) { var m = { parents: [Axis] }; m.name = name; m.prop = prop; m.invertable = invertable; m.inverted = 0; return m; }, clone: func() { var m = { parents: [Axis] }; m.name = me.name; m.prop = me.prop; m.invertable = me.invertable; m.inverted = me.inverted; return m; }, match: func(prop) { return 0; }, parse: func(prop) { }, readProps: func(props) {}, getName: func() { return me.name; }, getBinding: func(axis) { return props.Node.new(); }, isInvertable: func() { return me.invertable; }, isInverted: func() { return me.inverted; }, setInverted: func(b) { if (me.invertable) me.inverted = b; }, }; var CustomAxis = { new: func() { var m = { parents: [CustomAxis, Axis.new("Custom", "", 0) ] }; me.custom_binding = nil; return m; }, clone: func() { var m = { parents: [CustomAxis, Axis.new("Custom", "", 0) ] }; m.custom_binding = me.custom_binding; return m; }, match: func(prop) { var p = props.Node.new(); if (prop.getNode("binding") != nil) { props.copy(prop.getNode("binding"), p); me.custom_binding = p; return 1; } else { return 0; } }, getBinding: func(axis) { var p = props.Node.new().getNode("axis[" ~ axis ~ "]", 1); p.getNode("desc", 1).setValue(me.name); props.copy(me.custom_binding, p.getNode("binding", 1)); return p; }, }; var UnboundAxis = { new: func() { var m = { parents: [UnboundAxis, Axis.new("None", "", 0) ] }; return m; }, clone: func() { var m = { parents: [UnboundAxis, Axis.new("None", "", 0) ] }; return m; }, match: func(prop) { return 1; }, getBinding: func(axis) { return nil; }, }; var PropertyScaleAxis = { new: func(name, prop, factor=1, offset=0) { var m = { parents: [PropertyScaleAxis, Axis.new(name, prop, 1) ] }; m.prop=prop; m.factor = factor; m.offset = offset; return m; }, clone: func() { var m = { parents: [PropertyScaleAxis, Axis.new(me.name, me.prop, 1) ] }; m.inverted = me.inverted; m.prop= me.prop; m.factor = me.factor; m.offset = me.offset; return m; }, match: func(prop) { var cmd = prop.getNode("binding", 1).getNode("command", 1).getValue(); var p = prop.getNode("binding", 1).getNode("property", 1).getValue(); return ((cmd == "property-scale") and (p == me.prop)); }, parse: func(p) { bindingNode = p.getNode("binding", 1); me.prop = bindingNode.getNode("property", 1).getValue(); # Don't create an empty 'factor' node if it doesn't exist! # (value 0 wouldn't be appropriate for a default factor) factorNode = bindingNode.getNode("factor", 0); me.factor = (factorNode != nil) ? factorNode.getValue() : 1.0; me.inverted = (me.factor < 0); me.offset = bindingNode.getNode("offset", 1).getValue(); }, getBinding: func(axis) { var p = props.Node.new(); p = p.getNode("axis[" ~ axis ~ "]", 1); p.getNode("desc", 1).setValue(me.name); p.getNode("binding", 1).getNode("command", 1).setValue("property-scale"); p.getNode("binding", 1).getNode("property", 1).setValue(me.prop); if (me.inverted) { p.getNode("binding", 1).getNode("factor", 1).setValue(0 - me.factor); } else { p.getNode("binding", 1).getNode("factor", 1).setValue(me.factor); } p.getNode("binding", 1).getNode("offset", 1).setValue(me.offset); return p; }, }; var NasalScaleAxis = { new: func(name, script, prop) { var m = { parents: [NasalScaleAxis, Axis.new(name, prop, 0) ] }; m.script = script; m.prop = prop; return m; }, clone: func() { var m = { parents: [NasalScaleAxis, Axis.new(me.name, me.prop, 0) ] }; m.script = me.script; m.prop = me.prop; return m; }, match: func(prop) { var cmd = prop.getNode("binding", 1).getNode("command", 1).getValue(); var p = prop.getNode("binding", 1).getNode("script", 1).getValue(); if ((p != nil) and (cmd == "nasal")) { p = string.trim(p); p = string.replace(p, ";", ""); p = p ~ ";"; return (p == me.script); } else { return 0; } }, getBinding: func(axis) { var p = props.Node.new().getNode("axis[" ~ axis ~ "]", 1); p.getNode("desc", 1).setValue(me.name); p.getNode("binding", 1).getNode("command", 1).setValue("nasal"); p.getNode("binding", 1).getNode("script", 1).setValue(me.script); return p; }, }; var NasalLowHighAxis = { new: func(name, lowscript, highscript, prop) { var m = { parents: [NasalLowHighAxis, Axis.new(name, prop, 1) ] }; m.lowscript = lowscript; m.highscript = highscript; return m; }, clone: func() { var m = { parents: [NasalLowHighAxis, Axis.new(me.name, me.prop, 1) ] }; m.inverted = me.inverted; m.lowscript = me.lowscript; m.highscript = me.highscript; return m; }, match: func(prop) { var cmd = prop.getNode("low", 1).getNode("binding", 1).getNode("command", 1).getValue(); var p = prop.getNode("low", 1).getNode("binding", 1).getNode("script", 1).getValue(); if ((p == nil) or (cmd != "nasal")) return 0; p = string.trim(p); p = string.replace(p, ";", ""); p = p ~ ";"; if (p == me.lowscript) { return 1; } if (p == me.highscript) { me.inverted = 1; return 1; } return 0; }, getBinding: func(axis) { var p = props.Node.new().getNode("axis[" ~ axis ~ "]", 1); p.getNode("desc", 1).setValue(me.name); p.getNode("low", 1).getNode("binding", 1).getNode("command", 1).setValue("nasal"); p.getNode("high", 1).getNode("binding", 1).getNode("command", 1).setValue("nasal"); if (me.inverted) { p.getNode("low", 1).getNode("binding", 1).getNode("script", 1).setValue(me.highscript); p.getNode("high", 1).getNode("binding", 1).getNode("script", 1).setValue(me.lowscript); } else { p.getNode("low", 1).getNode("binding", 1).getNode("script", 1).setValue(me.lowscript); p.getNode("high", 1).getNode("binding", 1).getNode("script", 1).setValue(me.highscript); } return p; }, }; var axisBindings = [ PropertyScaleAxis.new("Aileron", "/controls/flight/aileron"), PropertyScaleAxis.new("Elevator", "/controls/flight/elevator"), PropertyScaleAxis.new("Rudder", "/controls/flight/rudder"), NasalScaleAxis.new("Throttle", "controls.throttleAxis();", "/controls/engines/engine[0]/throttle") , NasalScaleAxis.new("Mixture", "controls.mixtureAxis();", "/controls/engines/engine[0]/mixture") , NasalScaleAxis.new("Propeller", "controls.propellerAxis();", "/controls/engines/engine[0]/propeller-pitch") , NasalLowHighAxis.new("View (horizontal)", "setprop(\"/sim/current-view/goal-heading-offset-deg\", getprop(\"/sim/current-view/goal-heading-offset-deg\") + 30);", "setprop(\"/sim/current-view/goal-heading-offset-deg\", getprop(\"/sim/current-view/goal-heading-offset-deg\") - 30);", "/sim/current-view/goal-heading-offset-deg"), NasalLowHighAxis.new("View (vertical)", "setprop(\"/sim/current-view/goal-pitch-offset-deg\", getprop(\"/sim/current-view/goal-pitch-offset-deg\") - 20);", "setprop(\"/sim/current-view/goal-pitch-offset-deg\", getprop(\"/sim/current-view/goal-pitch-offset-deg\") + 20);", "/sim/current-view/goal-heading-offset-deg"), # PropertyScaleAxis.new("Aileron Trim", "/controls/flight/aileron-trim"), # PropertyScaleAxis.new("Elevator Trim", "/controls/flight/elevator-trim"), # PropertyScaleAxis.new("Rudder Trim", "/controls/flight/rudder-trim"), PropertyScaleAxis.new("Brake Left", "/controls/gear/brake-left", 0.5, 1.0), PropertyScaleAxis.new("Brake Right", "/controls/gear/brake-right", 0.5, 1.0), NasalLowHighAxis.new("Aileron Trim", "controls.aileronTrim(-1);", "controls.aileronTrim(1);", "/controls/flight/aileron-trim"), NasalLowHighAxis.new("Elevator Trim", "controls.elevatorTrim(-1);", "controls.elevatorTrim(1);", "/controls/flight/elevator-trim"), NasalLowHighAxis.new("Rudder Trim", "controls.rudderTrim(-1);", "controls.rudderTrim(1);", "/controls/flight/rudder-trim"), CustomAxis.new(), UnboundAxis.new(), ]; # Button bindings var ButtonBinding = { new: func(name, binding, repeatable) { var m = { parents: [ButtonBinding] }; m.name = name; m.binding = binding; m.repeatable = repeatable; return m; }, clone: func() { var m = { parents: [ButtonBinding] }; m.name = me.name; m.binding= me.binding; m.repeatable = me.repeatable; return m; }, match: func(prop) { return 0; }, getName: func() { return me.name; }, getBinding: func(button) { return nil; }, isRepeatable: func() { return me.repeatable; } }; var CustomButton = { new: func() { var m = { parents: [CustomButton, ButtonBinding.new("Custom", "", 0) ] }; m.custom_binding = nil; return m; }, clone: func() { var m = { parents: [CustomButton, ButtonBinding.new("Custom", "", 0) ] }; m.custom_binding = me.custom_binding; return m; }, match: func(prop) { if (prop.getNode("binding") != nil) { var p = props.Node.new(); props.copy(prop.getNode("binding"), p); me.custom_binding = p; return 1; } else { return 0; } }, getBinding: func(button) { var p = props.Node.new().getNode("button[" ~ button ~ "]", 1); p.getNode("desc", 1).setValue(me.name); props.copy(me.custom_binding, p.getNode("binding", 1)); return p; }, }; var UnboundButton = { new: func() { var m = { parents: [UnboundButton, ButtonBinding.new("None", "", 0) ] }; return m; }, clone: func() { var m = { parents: [UnboundButton, ButtonBinding.new("None", "", 0) ] }; return m; }, match: func(prop) { return (prop.getNode("binding") != nil); }, getBinding: func(button) { var p = props.Node.new().getNode("button[" ~ button ~ "]", 1); p.getNode("desc", 1).setValue(me.name); return p; }, }; var PropertyToggleButton = { new: func(name, prop) { var m = { parents: [PropertyToggleButton, ButtonBinding.new(name, prop, 0) ] }; return m; }, clone: func() { var m = { parents: [PropertyToggleButton, ButtonBinding.new(me.name, me.binding, 0) ] }; return m; }, match: func(prop) { var c = prop.getNode("binding", 1).getNode("command", 1).getValue(); var p = prop.getNode("binding", 1).getNode("property", 1).getValue(); return ((c == "property-toggle") and (p == me.prop)); }, getBinding: func(button) { var p = props.Node.new().getNode("button[" ~ button ~ "]", 1); p.getNode("desc", 1).setValue(me.name); p.getNode("binding", 1).getNode("command", 1).setValue("property-toggle"); p.getNode("binding", 1).getNode("property", 1).setValue(me.binding); return p; }, }; var PropertyAdjustButton = { new: func(name, prop, step) { var m = { parents: [PropertyAdjustButton, ButtonBinding.new(name, prop, 0) ] }; m.step = step; return m; }, clone: func() { var m = { parents: [PropertyAdjustButton, ButtonBinding.new(me.name, me.binding, 0) ] }; m.step = me.step; return m; }, match: func(prop) { var c = prop.getNode("binding", 1).getNode("command", 1).getValue(); var p = prop.getNode("binding", 1).getNode("property", 1).getValue(); var s = prop.getNode("binding", 1).getNode("step", 1).getValue(); return ((c == "property-adjust") and (p == me.binding) and (s == me.step)); }, getBinding: func(button) { var p = props.Node.new().getNode("button[" ~ button ~ "]", 1); p.getNode("desc", 1).setValue(me.name); p.getNode("binding", 1).getNode("command", 1).setValue("property-adjust"); p.getNode("binding", 1).getNode("property", 1).setValue(me.binding); p.getNode("binding", 1).getNode("step", 1).setValue(me.step); return p; }, }; var NasalButton = { new: func(name, script, repeatable) { var m = { parents: [NasalButton, ButtonBinding.new(name, script, repeatable) ] }; return m; }, clone: func() { var m = { parents: [NasalButton, ButtonBinding.new(me.name, me.binding, me.repeatable) ] }; return m; }, match: func(prop) { var c = prop.getNode("binding", 1).getNode("command", 1).getValue(); var p = prop.getNode("binding", 1).getNode("script", 1).getValue(); if (p == nil) { return 0; } p = string.trim(p); p = string.replace(p, ";", ""); p = p ~ ";"; return ((c == "nasal") and (p == me.binding)); }, getBinding: func(button) { var p = props.Node.new().getNode("button[" ~ button ~ "]", 1); p.getNode("desc", 1).setValue(me.name); p.getNode("binding", 1).getNode("command", 1).setValue("nasal"); p.getNode("binding", 1).getNode("script", 1).setValue(me.binding); p.getNode("repeatable", 1).setValue(me.repeatable); return p; }, }; var NasalHoldButton = { new: func(name, script, scriptUp) { var m = { parents: [NasalHoldButton, ButtonBinding.new(name, script, 0) ] }; m.scriptUp = scriptUp; return m; }, clone: func() { var m = { parents: [NasalHoldButton, ButtonBinding.new(me.name, me.binding, 0) ] }; m.scriptUp = me.scriptUp; return m; }, match: func(prop) { var c = prop.getNode("mod-up", 1).getNode("binding", 1).getNode("command", 1).getValue(); var p1 = prop.getNode("binding", 1).getNode("script", 1).getValue(); var p2 = prop.getNode("mod-up", 1).getNode("binding", 1).getNode("script", 1).getValue(); if (p2 == nil) { return 0; } p1 = string.trim(p1); p1 = string.replace(p1, ";", ""); p1 = p1 ~ ";"; return ((c == "nasal") and (p1 == me.binding)); }, getBinding: func(button) { var p = props.Node.new().getNode("button[" ~ button ~ "]", 1); p.getNode("desc", 1).setValue(me.name); p.getNode("repeatable", 1).setValue("false"); p.getNode("binding", 1).getNode("command", 1).setValue("nasal"); p.getNode("binding", 1).getNode("script", 1).setValue(me.binding); p.getNode("mod-up", 1).getNode("binding", 1).getNode("command", 1).setValue("nasal"); p.getNode("mod-up", 1).getNode("binding", 1).getNode("script", 1).setValue(me.scriptUp); return p; }, }; var buttonBindings = [ NasalButton.new("Throttle Up", "controls.incThrottle(0.01, 1.0);", 1), NasalButton.new("Throttle Down", "controls.incThrottle(-0.01, -1.0);", 1), NasalButton.new("Mixture Rich", "controls.adjMixture(1);", 1), NasalButton.new("Mixture Lean", "controls.adjMixture(-1);", 1), NasalButton.new("Propeller Fine", "controls.adjPropeller(1);", 1), NasalButton.new("Propeller Coarse", "controls.adjPropeller(-1);", 1), NasalButton.new("Elevator Trim Up", "controls.elevatorTrim(-1);", 1), NasalButton.new("Elevator Trim Down", "controls.elevatorTrim(1);", 1), NasalButton.new("Rudder Trim Left", "controls.rudderTrim(-1);", 1), NasalButton.new("Rudder Trim Right", "controls.rudderTrim(1);", 1), NasalButton.new("Aileron Trim Left", "controls.aileronTrim(-1);", 1), NasalButton.new("Aileron Trim Right", "controls.aileronTrim(1);", 1), NasalHoldButton.new("FGCom PTT", "controls.ptt(1);", "controls.ptt(0);"), NasalHoldButton.new("Trigger", "controls.trigger(1);", "controls.trigger(0);"), NasalHoldButton.new("Flaps Up", "controls.flapsDown(-1);", "controls.flapsDown(0);"), NasalHoldButton.new("Flaps Down", "controls.flapsDown(1);", "controls.flapsDown(0);"), NasalHoldButton.new("Gear Up", "controls.gearDown(-1);", "controls.gearDown(0);"), NasalHoldButton.new("Gear Down", "controls.gearDown(1);", "controls.gearDown(0);"), NasalHoldButton.new("Spoilers Retract", "controls.stepSpoilers(-1);", "controls.stepSpoilers(0);"), NasalHoldButton.new("Spoilers Deploy", "controls.stepSpoilers(1);", "controls.stepSpoilers(0);"), NasalHoldButton.new("Brakes", "controls.applyBrakes(1);", "controls.applyBrakes(0);"), NasalButton.new("View Decrease", "view.decrease(0.75);", 1), NasalButton.new("View Increase", "view.increase(0.75);", 1), NasalButton.new("View Cycle Forwards", "view.stepView(1);", 0), NasalButton.new("View Cycle Backwards", "view.stepView(-1);", 0), PropertyAdjustButton.new("View Left", "/sim/current-view/goal-heading-offset-deg", "30.0"), PropertyAdjustButton.new("View Right", "/sim/current-view/goal-heading-offset-deg", "-30.0"), PropertyAdjustButton.new("View Up", "/sim/current-view/goal-pitch-offset-deg", "20.0"), PropertyAdjustButton.new("View Down", "/sim/current-view/goal-pitch-offset-deg", "-20.0"), CustomButton.new(), ]; # Parse config from the /input tree and write it to the # dialog_root. var readConfig = func(dialog_root="/sim/gui/dialogs/joystick-config") { var js_name = getprop(dialog_root ~ "/selected-joystick"); var joysticks = props.globals.getNode("/input/joysticks").getChildren("js"); if (size(joysticks) == 0) { return 0; } if (js_name == nil) { js_name = joysticks[0].getNode("id").getValue(); } var js = nil; forindex (var i; joysticks) { if ((joysticks[i].getNode("id") != nil) and (joysticks[i].getNode("id").getValue() == js_name)) { js = joysticks[i]; setprop(dialog_root ~ "/selected-joystick", js_name); setprop(dialog_root ~ "/selected-joystick-index", i); setprop(dialog_root ~ "/selected-joystick-config", joysticks[i].getNode("source").getValue()); } } if (js == nil) { # We didn't find the joystick we expected - default to the first setprop(dialog_root ~ "/selected-joystick", joysticks[0].getNode("id").getValue()); setprop(dialog_root ~ "/selected-joystick-index", 0); setprop(dialog_root ~ "/selected-joystick-config", joysticks[0].getNode("source").getValue()); } # Set up the axes assignments var axes = js.getChildren("axis"); for (var axis = 0; axis < MAX_AXES; axis = axis +1) { var p = props.globals.getNode(dialog_root ~ "/axis[" ~ axis ~ "]", 1); p.remove(); p = props.globals.getNode(dialog_root ~ "/axis[" ~ axis ~ "]", 1); # Note that we can't simply use an index into the axes array # as that doesn't work for a sparsley populated set of axes. # E.g. one with n="3" var a = js.getNode("axis[" ~ axis ~ "]"); if (a != nil) { # Read properties from bindings props.copy(a, p.getNode("original_binding", 1)); var binding = nil; foreach (var b; joystick.axisBindings) { if ((binding == nil) and (a != nil) and b.match(a)) { binding = b.clone(); binding.parse(a); p.getNode("binding", 1).setValue(binding.getName()); p.getNode("invertable", 1).setValue(binding.isInvertable()); p.getNode("inverted", 1).setValue(binding.isInverted()); } } if (binding == nil) { # No binding for this axis p.getNode("binding", 1).setValue("None"); p.getNode("invertable", 1).setValue(0); p.getNode("inverted", 1).setValue(0); p.removeChild("original_binding"); } } else { p.getNode("binding", 1).setValue("None"); p.getNode("invertable", 1).setValue(0); p.getNode("inverted", 1).setValue(0); p.removeChild("original_binding"); } } # Set up button assignment. var buttons = js.getChildren("button"); for (var button = 0; button < MAX_BUTTONS; button = button + 1) { var btn = props.globals.getNode(dialog_root ~ "/button[" ~ button ~ "]", 1); btn.remove(); btn = props.globals.getNode(dialog_root ~ "/button[" ~ button ~ "]", 1); # Note that we can't simply use an index into the buttons array # as that doesn't work for a sparsley populated set of buttons. # E.g. one with n="3" var a = js.getNode("button[" ~ button ~ "]"); if (a != nil) { # Read properties from bindings props.copy(a, btn.getNode("original_binding", 1)); var binding = nil; foreach (var b; joystick.buttonBindings) { if ((binding == nil) and (a != nil) and b.match(a)) { binding = b.clone(); btn.getNode("binding", 1).setValue(binding.getName()); props.copy(b.getBinding(button), btn.getNode("original_binding", 1)); } } if (b == nil) { btn.getNode("binding", 1).setValue("None"); btn.removeChild("original_binding"); } } else { btn.getNode("binding", 1).setValue("None"); btn.removeChild("original_binding"); } } # Set up Nasal code. var nasals = js.getChildren("nasal"); for (var nasal = 0; nasal < MAX_NASALS; nasal = nasal + 1) { var nas = props.globals.getNode(dialog_root ~ "/nasal[" ~ nasal ~ "]", 1); nas.remove(); nas = props.globals.getNode(dialog_root ~ "/nasal[" ~ nasal ~ "]", 1); # Note that we can't simply use an index into the buttons array # as that doesn't work for a sparsley populated set of buttons. # E.g. one with n="3" var a = js.getNode("nasal[" ~ nasal ~ "]"); if (a != nil) { props.copy(a, nas.getNode("original_script", 1)); } } } var writeConfig = func(dialog_root="/sim/gui/dialogs/joystick-config", reset=0) { # Write out the joystick file. var config = props.Node.new(); var id = getprop(dialog_root ~ "/selected-joystick"); if (reset == 1) { # We've been asked to reset the joystick config to the default. As we can't # delete the configuration file, we achieve this by setting an invalid name # tag that won't match. config.getNode("name", 1).setValue("UNUSED INVALID CONFIG"); } else { config.getNode("name", 1).setValue(id); } var nasals = props.globals.getNode(dialog_root).getChildren("nasal"); forindex (var nas; nasals) { var nasalscript = config.getNode("nasal[" ~ nas ~ "]", 1); props.copy(props.globals.getNode(dialog_root ~ "/nasal[" ~ nas ~ "]/original_script", 1), nasalscript); } var axes = props.globals.getNode(dialog_root).getChildren("axis"); forindex (var axis; axes) { var name = getprop(dialog_root ~ "/axis[" ~ axis ~ "]/binding"); if (name != "None") { foreach (var binding; axisBindings) { if (binding.getName() == name) { var b = binding.clone(); b.setInverted(getprop(dialog_root ~ "/axis[" ~ axis ~ "]/inverted")); # Generate the axis and binding var axisnode = config.getNode("axis[" ~ axis ~ "]", 1); if (name == "Custom") { props.copy(props.globals.getNode(dialog_root ~ "/axis[" ~ axis ~ "]/original_binding", 1), axisnode); } else { props.copy(b.getBinding(axis), axisnode); } } } } } var buttons = props.globals.getNode(dialog_root).getChildren("button"); forindex (var btn; buttons) { var name = getprop(dialog_root ~ "/button[" ~ btn ~ "]/binding"); if (name != "None") { foreach (var binding; buttonBindings) { if (binding.getName() == name) { var b = binding.clone(); # Generate the axis and binding var buttonprop = config.getNode("button[" ~ btn ~ "]", 1); if (name == "Custom") { props.copy(props.globals.getNode(dialog_root ~ "/button[" ~ btn ~ "]/original_binding", 1), buttonprop); } else { props.copy(b.getBinding(btn), buttonprop); } } } } } var filename = id; filename = string.replace(filename, " ", "-"); filename = string.replace(filename, ".", ""); filename = string.replace(filename, "/", ""); # Write out the file io.write_properties(getprop("/sim/fg-home") ~ "/Input/Joysticks/" ~ filename ~ ".xml", config); } var resetConfig = func(dialog_root="/sim/gui/dialogs/joystick-config") { writeConfig(dialog_root, 1); }