Merge /u/jsb1685/fgdata/ branch next into next
https://sourceforge.net/p/flightgear/fgdata/merge-requests/164/
This commit is contained in:
commit
4b0d08975c
5 changed files with 357 additions and 84 deletions
5
Nasal/README.txt
Normal file
5
Nasal/README.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
FGDATA/Nasal/ contains nasal core modules (*.nas).
|
||||
All .nas files in this directory will be loaded automatically while Flightgear starts.
|
||||
|
||||
The first level sub-directories will be scanned for .nas files and this files will be
|
||||
also loaded while Flightgear starts unless disabled in defaults.xml.
|
|
@ -30,110 +30,35 @@
|
|||
# For more details, see $FG_ROOT/Docs/README.add-ons.
|
||||
|
||||
# hashes to store listeners and timers per addon ID
|
||||
var _listeners = {};
|
||||
var _timers = {};
|
||||
var _orig_setlistener = nil;
|
||||
var _orig_maketimer = nil;
|
||||
var _modules = {};
|
||||
|
||||
var getNamespaceName = func(a) {
|
||||
return "__addon[" ~ a.id ~ "]__";
|
||||
}
|
||||
|
||||
var load = func(a) {
|
||||
var main_nas = a.basePath ~ "/addon-main.nas";
|
||||
var namespace = getNamespaceName(a);
|
||||
var loaded = a.node.getNode("loaded", 1);
|
||||
loaded.setBoolValue(0);
|
||||
|
||||
if (globals[namespace] == nil) {
|
||||
globals[namespace] = {};
|
||||
}
|
||||
|
||||
_listeners[a.id] = [];
|
||||
_timers[a.id] = [];
|
||||
|
||||
# redirect setlistener() for addon
|
||||
globals[namespace].setlistener = func(p, f, start=0, runtime=1) {
|
||||
# listeners won't work on aliases so find real node
|
||||
if (typeof(p) == "scalar") {
|
||||
p = props.getNode(p, 1);
|
||||
while (p.getAttribute("alias")) {
|
||||
p = p.getAliasTarget();
|
||||
}
|
||||
}
|
||||
append(_listeners[a.id], _orig_setlistener(p, f, start, runtime));
|
||||
logprint(2, "setlistener " ~ p.getPath() ~ " (" ~
|
||||
size(_listeners[a.id]) ~ " listener(s) tracked for " ~
|
||||
a.id ~ ")");
|
||||
}
|
||||
|
||||
# redirect maketimer for addon
|
||||
globals[namespace].maketimer = func() {
|
||||
if (size(arg) == 2) {
|
||||
append(_timers[a.id], _orig_maketimer(arg[0], arg[1]));
|
||||
} elsif (size(arg) == 3) {
|
||||
append(_timers[a.id], _orig_maketimer(arg[0], arg[1], arg[2]));
|
||||
} else {
|
||||
logprint(5, "Invalid number of arguments to maketimer()");
|
||||
return;
|
||||
}
|
||||
logprint(2, size(_timers[a.id]) ~ " timer(s) tracked for " ~ a.id);
|
||||
return _timers[a.id][-1];
|
||||
}
|
||||
|
||||
if (io.load_nasal(main_nas, namespace)) {
|
||||
var module = modules.Module.new(a.id, namespace, a.node);
|
||||
module.setFilePath(a.basePath);
|
||||
module.setMainFile("addon-main.nas");
|
||||
|
||||
if (module.load(a) != nil) {
|
||||
logprint(5, "[OK] '" ~ a.name ~ "' (V. " ~ a.version.str() ~
|
||||
") loaded.");
|
||||
var addon_main = globals[namespace]["main"];
|
||||
var addon_main_args = [a];
|
||||
var errors = [];
|
||||
call(addon_main, addon_main_args, errors);
|
||||
if (size(errors)) {
|
||||
debug.printerror(errors);
|
||||
} else {
|
||||
loaded.setBoolValue(1);
|
||||
}
|
||||
logprint(3, "Tracked resources after running the main() function of " ~
|
||||
a.id ~ ":");
|
||||
logprint(3, "#listeners: " ~ size(_listeners[a.id]));
|
||||
logprint(3, "#timers: " ~ size(_timers[a.id]));
|
||||
logprint(3, "Use log level DEBUG to see all calls to the " ~
|
||||
"setlistener() and maketimer() wrappers.");
|
||||
module.printTrackedResources();
|
||||
} else {
|
||||
logprint(5, "Failed loading addon-main.nas for " ~ a.id);
|
||||
}
|
||||
_modules[a.id] = module;
|
||||
}
|
||||
|
||||
|
||||
var remove = func(a) {
|
||||
if (!a.node.getChild("loaded").getValue()) {
|
||||
logprint(5, "! ", a.id, " was not fully loaded.");
|
||||
}
|
||||
|
||||
logprint(5, "- Removing add-on ", a.id);
|
||||
var namespace = getNamespaceName(a);
|
||||
foreach (var id; _listeners[a.id]) {
|
||||
logprint(3, "Removing listener " ~ id);
|
||||
removelistener(id);
|
||||
}
|
||||
_listeners[a.id] = [];
|
||||
|
||||
logprint(3, "Stopping timers ");
|
||||
foreach (var t; _timers[a.id]) {
|
||||
if (typeof(t.stop) == "func") {
|
||||
t.stop();
|
||||
logprint(2, " .");
|
||||
}
|
||||
}
|
||||
_timers[a.id] = [];
|
||||
|
||||
# call clean up method if available
|
||||
# addon shall release resources not handled by addon framework
|
||||
if (globals[namespace]["unload"] != nil
|
||||
and typeof(globals[namespace]["unload"]) == "func") {
|
||||
globals[namespace].unload(a);
|
||||
}
|
||||
globals[namespace] = {};
|
||||
_modules[a.id].unload();
|
||||
}
|
||||
|
||||
var _reloadFlags = {};
|
||||
|
|
333
Nasal/modules.nas
Normal file
333
Nasal/modules.nas
Normal file
|
@ -0,0 +1,333 @@
|
|||
#-------------------------------------------------------------------------------
|
||||
# modules.nas - Nasal module helper for Add-ons and re-loadable modules
|
||||
# author: jsb
|
||||
# created: 12/2019
|
||||
#-------------------------------------------------------------------------------
|
||||
# modules.nas allowes to load and unload Nasal modules at runtime (e.g. without
|
||||
# restarting Flightgear as a whole). It implements resource tracking for
|
||||
# setlistener and maketimer to make unloading easier
|
||||
#-------------------------------------------------------------------------------
|
||||
# Example - generic module load:
|
||||
#
|
||||
# if (modules.isAvailable("foo_bar")) {
|
||||
# modules.load("foo_bar");
|
||||
# }
|
||||
#
|
||||
# Example - create an aircraft nasal system as module
|
||||
# (e.g. for rapid reload while development)
|
||||
#
|
||||
# var my_foo_sys = modules.Module.new("my_aircraft_foo");
|
||||
# my_foo_sys.setDebug(1);
|
||||
# my_foo_sys.setFilePath(getprop("/sim/aircraft-dir")~"/Nasal");
|
||||
# my_foo_sys.setMainFile("foo.nas");
|
||||
# my_foo_sys.load();
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
var MODULES_DIR = getprop("/sim/fg-root")~"/Nasal/Modules/";
|
||||
var MODULES_NODE = nil;
|
||||
var _modules_available = {};
|
||||
var _instances = {};
|
||||
|
||||
# Class to handle a nasal module at runtime
|
||||
var Module = {
|
||||
_orig_setlistener: setlistener,
|
||||
_orig_maketimer: maketimer,
|
||||
_orig_settimer: settimer,
|
||||
|
||||
# id: must be a string without special characters or spaces
|
||||
# ns: optional namespace name
|
||||
# node: optional property node for module management
|
||||
new: func(id, ns = "", node = nil) {
|
||||
if (!id) {
|
||||
id = "_module_manager";
|
||||
}
|
||||
if (_instances[id] != nil) {
|
||||
return _instances[id];
|
||||
}
|
||||
var obj = {
|
||||
parents: [me],
|
||||
_listeners: [],
|
||||
_timers: [],
|
||||
_debug: 0,
|
||||
|
||||
id: id,
|
||||
version: 1,
|
||||
file_path: MODULES_DIR,
|
||||
main_file: "main.nas",
|
||||
namespace: ns ? ns : id,
|
||||
node: nil,
|
||||
};
|
||||
if (isa(node, props.Node)) {
|
||||
obj.node = node
|
||||
} else {
|
||||
obj.node = MODULES_NODE.getNode(id, 1);
|
||||
}
|
||||
obj.reloadN = obj.node.initNode("reload", 0, "BOOL");
|
||||
obj.loadedN = obj.node.initNode("loaded", 0, "BOOL");
|
||||
obj.lcountN = obj.node.initNode("listeners", 0, "INT");
|
||||
obj.tcountN = obj.node.initNode("timers", 0, "INT");
|
||||
obj.lhitN = obj.node.initNode("listener-hits", 0, "INT");
|
||||
|
||||
obj.reloadL = setlistener(obj.reloadN, func(n) {
|
||||
if (n.getValue()) {
|
||||
n.setValue(0);
|
||||
logprint(3, "Reload listener for ", obj.id, " triggered (",
|
||||
obj.reloadL, ")");
|
||||
obj.reload();
|
||||
}
|
||||
});
|
||||
_instances[id] = obj;
|
||||
return obj;
|
||||
},
|
||||
|
||||
getNode: func { return me.node; },
|
||||
getReloadNode: func { return me.reloadN; },
|
||||
getNamespaceName: func { return me.namespace; },
|
||||
getNamespace: func { return globals[me.namespace]; },
|
||||
getFilePath: func { return me.file_path; },
|
||||
|
||||
#return variable from module namespace
|
||||
get: func(var_name) {
|
||||
return globals[me.namespace][var_name];
|
||||
},
|
||||
|
||||
setDebug: func (debug = 1) {
|
||||
me._debug = debug;
|
||||
logprint(4, "Module "~me.id~" debug = "~debug);
|
||||
},
|
||||
|
||||
setFilePath: func(path) {
|
||||
if (io.is_directory(path)) {
|
||||
if (substr(path, -1) != "/")
|
||||
path ~= "/";
|
||||
me.file_path = path;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
|
||||
setMainFile: func(filename) {
|
||||
if (typeof(filename) == "scalar") {
|
||||
me.main_file = filename;
|
||||
}
|
||||
else {
|
||||
logprint("4", "setMainFile() needs a string parameter");
|
||||
}
|
||||
},
|
||||
|
||||
setNamespace: func(ns) {
|
||||
if (typeof(ns) == "scalar") {
|
||||
me.namespace = ns;
|
||||
}
|
||||
else {
|
||||
logprint("4", "setNamespace() needs a string parameter");
|
||||
}
|
||||
},
|
||||
|
||||
# load module
|
||||
# if no arguments are given, the Module object will be passed to main()
|
||||
load: func(myargs...) {
|
||||
me.loadedN.setBoolValue(0);
|
||||
|
||||
if (globals[me.namespace] == nil) {
|
||||
globals[me.namespace] = {};
|
||||
}
|
||||
logprint(5, "Module.load() ", me.id);
|
||||
me.lcountN.setIntValue(0);
|
||||
me.tcountN.setIntValue(0);
|
||||
me.lhitN.setIntValue(0);
|
||||
me._redirect_setlistener();
|
||||
me._redirect_maketimer();
|
||||
me._redirect_settimer();
|
||||
|
||||
var filename = me.file_path~"/"~me.main_file;
|
||||
if (io.load_nasal(filename, me.namespace)) {
|
||||
var main = globals[me.namespace]["main"];
|
||||
if (typeof(main) == "func") {
|
||||
var module_args = [];
|
||||
if (size(myargs) == 0) module_args = [me];
|
||||
else module_args = myargs;
|
||||
var errors = [];
|
||||
call(main, module_args, errors);
|
||||
if (size(errors)) {
|
||||
debug.printerror(errors);
|
||||
} else {
|
||||
me.loadedN.setBoolValue(1);
|
||||
}
|
||||
} else {
|
||||
me.loadedN.setBoolValue(1);
|
||||
}
|
||||
return me;
|
||||
}
|
||||
else { return nil; }
|
||||
},
|
||||
|
||||
#-- unload a module and its tracked resources --
|
||||
unload: func() {
|
||||
if (!me.loadedN.getValue()) {
|
||||
logprint(5, "! ", me.id, " was not fully loaded.");
|
||||
}
|
||||
if (globals[me.namespace] != nil
|
||||
and typeof(globals[me.namespace]) == "hash")
|
||||
{
|
||||
logprint(5, "- Removing module ", me.id);
|
||||
if (globals[me.namespace]["setlistener"] != nil)
|
||||
globals[me.namespace]["setlistener"] = func {};
|
||||
foreach (var id; me._listeners) {
|
||||
logprint(3, "Removing listener "~id);
|
||||
if (removelistener(id)) {
|
||||
me.lcountN.setValue(me.lcountN.getValue() - 1);
|
||||
}
|
||||
}
|
||||
me._listeners = [];
|
||||
|
||||
logprint(3, "Stopping timers ");
|
||||
if (globals[me.namespace]["maketimer"] != nil)
|
||||
globals[me.namespace]["maketimer"] = func {};
|
||||
foreach (var t; me._timers) {
|
||||
if (typeof(t.stop) == "func") {
|
||||
t.stop();
|
||||
me.tcountN.setValue(me.tcountN.getValue() - 1);
|
||||
logprint(2, " .");
|
||||
}
|
||||
}
|
||||
me._timers = [];
|
||||
|
||||
# call clean up method if available
|
||||
# module shall release resources not handled by this framework
|
||||
if (globals[me.namespace]["unload"] != nil
|
||||
and typeof(globals[me.namespace]["unload"]) == "func") {
|
||||
var errors = [];
|
||||
call(globals[me.namespace].unload, [me], errors);
|
||||
if (size(errors)) {
|
||||
debug.printerror(errors);
|
||||
}
|
||||
}
|
||||
me.loadedN.setBoolValue(0);
|
||||
#kill namespace (replace with empty hash and hope GC cleans up behind us)
|
||||
globals[me.namespace] = nil;
|
||||
}
|
||||
},
|
||||
|
||||
reload: func() {
|
||||
me.unload();
|
||||
me.load();
|
||||
},
|
||||
|
||||
printTrackedResources: func(loglevel = 3) {
|
||||
logprint(loglevel, "Tracked resources after running the main() function of " ~
|
||||
me.id~":");
|
||||
logprint(loglevel, "#listeners: "~size(me._listeners));
|
||||
logprint(loglevel, "#timers: "~size(me._timers));
|
||||
logprint(loglevel, "Use log level DEBUG to see all calls to the " ~
|
||||
"setlistener() and maketimer() wrappers.");
|
||||
},
|
||||
|
||||
# redirect setlistener() for module
|
||||
_redirect_setlistener: func() {
|
||||
globals[me.namespace].setlistener = func(p, f, start=0, runtime=0) {
|
||||
if (!isa(p, props.Node)) {
|
||||
p = props.getNode(p, 1).resolveAlias();
|
||||
}
|
||||
|
||||
if (me._debug) {
|
||||
var f_debug = func {
|
||||
me.lhitN.setValue(me.lhitN.getValue() + 1);
|
||||
if (int(me._debug) > 1) {
|
||||
print("Listener hit for: ", p.getPath());
|
||||
}
|
||||
call(f, arg);
|
||||
};
|
||||
append(me._listeners, Module._orig_setlistener(p, f_debug, start, runtime));
|
||||
var c = caller(1);
|
||||
if (c != nil) {
|
||||
print(sprintf("setlistener for %s called from %s:%s",
|
||||
p.getPath(), io.basename(c[2]), c[3]));
|
||||
};
|
||||
} else {
|
||||
append(me._listeners, Module._orig_setlistener(p,
|
||||
f, start, runtime));
|
||||
}
|
||||
me.lcountN.setValue(me.lcountN.getValue() + 1);
|
||||
}
|
||||
me.setlistener = globals[me.namespace].setlistener;
|
||||
},
|
||||
|
||||
# redirect maketimer for module
|
||||
_redirect_maketimer: func() {
|
||||
globals[me.namespace].maketimer = func() {
|
||||
if (size(arg) == 2) {
|
||||
append(me._timers, Module._orig_maketimer(arg[0], arg[1]));
|
||||
} elsif (size(arg) == 3) {
|
||||
append(me._timers,
|
||||
Module._orig_maketimer(arg[0], arg[1], arg[2]));
|
||||
} else {
|
||||
logprint(5, "Invalid number of arguments to maketimer()");
|
||||
return;
|
||||
}
|
||||
if (me._debug) {
|
||||
var c = caller(1);
|
||||
if (c != nil) {
|
||||
print(sprintf("maketimer called from %s:%s",
|
||||
io.basename(c[2]), c[3]));
|
||||
};
|
||||
}
|
||||
me.tcountN.setValue(me.tcountN.getValue() + 1);
|
||||
return me._timers[-1];
|
||||
}
|
||||
me.maketimer = globals[me.namespace].maketimer;
|
||||
},
|
||||
|
||||
_redirect_settimer: func() {
|
||||
globals[me.namespace].settimer = func() {
|
||||
var c = caller(1);
|
||||
logprint(5, sprintf("\n\Unsupported settimer() call from %s:%s. "~
|
||||
"Use maketimer() instead.",
|
||||
io.basename(c[2]), c[3]));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
var isAvailable = func(name) {
|
||||
return contains(_modules_available, name);
|
||||
}
|
||||
|
||||
var _getInstance = func(name) {
|
||||
if (isAvailable(name) and _instances[name] == nil) {
|
||||
var m = Module.new(name);
|
||||
m.setFilePath(MODULES_DIR~name);
|
||||
}
|
||||
return _instances[name];
|
||||
}
|
||||
|
||||
var setDebug = func(name, debug=1) {
|
||||
if (isAvailable(name)) {
|
||||
var module = _getInstance(name);
|
||||
module.setDebug(debug);
|
||||
}
|
||||
}
|
||||
|
||||
var load = func(name, ns="") {
|
||||
var m = _getInstance(name);
|
||||
if (m != nil) {
|
||||
if (ns) { m.setNamespace(ns); }
|
||||
return m.load();
|
||||
}
|
||||
else return 0;
|
||||
}
|
||||
|
||||
# scan MODULES_DIR for subdirectories; it is assumed, that only well-formed
|
||||
# modules are stored in that directories, so no further checks right here
|
||||
var _findModules = func() {
|
||||
var module_dirs = io.subdirectories(MODULES_DIR);
|
||||
_modules_available = {};
|
||||
foreach (var name; module_dirs) {
|
||||
_modules_available[name] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
#props.getNode is available only after nasal dir has been initialized
|
||||
_setlistener("sim/signals/nasal-dir-initialized", func {
|
||||
MODULES_NODE = props.getNode("/nasal/modules", 1);
|
||||
_findModules();
|
||||
});
|
6
Nasal/modules/README-modules.txt
Normal file
6
Nasal/modules/README-modules.txt
Normal file
|
@ -0,0 +1,6 @@
|
|||
The folder FGDATA/Nasal/modules contains optional nasal modules.
|
||||
This modules will not be loaded automatically by Flightgear but can be
|
||||
loaded on demand when needed, e.g. by an aircraft developer.
|
||||
|
||||
For more information see FGDATA/Nasal/modules.nas and the Flightgear wiki at
|
||||
http://wiki.flightgear.org/Modules.nas
|
4
Nasal/modules/dummy.nas
Normal file
4
Nasal/modules/dummy.nas
Normal file
|
@ -0,0 +1,4 @@
|
|||
#------------------------------------------
|
||||
# dummy.nas
|
||||
# empty file to avoid error message
|
||||
#------------------------------------------
|
Loading…
Reference in a new issue