1
0
Fork 0
fgdata/Aircraft/Generic/MPCarriers.nas

638 lines
24 KiB
Text
Raw Normal View History

###############################################################################
##
## Nasal module for connecting a local AI carrier to a MP player.
##
## Copyright (C) 2007 - 2012 Anders Gidenstam (anders(at)gidenstam.org)
## Copyright (C) 2009 Vivian Meazza
## This file is licensed under the GPL license version 2 or later.
##
###############################################################################
# NOTE:
# This module is intended to be loaded under the name MPCarriers.
# To see what is happening with multiplayer/ai carriers, look in
# /ai/models/carrier[]/mp-control/.
#
# If this is true, we auto-enable ai scenarios and auto-attach ai carriers to
# mp carriers.
#
var g_auto_attach = props.globals.getNode("/sim/mp-carriers/auto-attach", 1);
var g_latch_always = props.globals.getNode("/sim/mp-carriers/latch-always", 1);
printf("fgdata/Aircraft/Generic/MPCarriers.nas: g_auto_attach.getValue()=%s g_latch_always=%s",
g_auto_attach.getValue(),
g_latch_always.getValue(),
);
# Constants
var lat = "position/latitude-deg";
var lon = "position/longitude-deg";
var alt = "position/altitude-ft";
var c_heading = "orientation/true-heading-deg";
var c_pitch = "orientation/pitch-deg";
var c_roll = "orientation/roll-deg";
var c_speed = "velocities/speed-kts";
var x = "position/global-x";
var y = "position/global-y";
var z = "position/global-z";
#var c_control_speed = "controls/base-speed-kts";
#var c_control_course = "controls/base-course-deg";
var c_control_speed = "controls/tgt-speed-kts";
var c_control_course = "controls/tgt-heading-degs";
var c_wave_off_lights = "controls/flols/wave-off-lights";
var c_control_mp_ctrl = "controls/mp-control";
var c_deck_elev = "controls/elevators";
var c_deck_lights = "controls/lighting/deck-lights";
var c_flood_lights = "controls/lighting/flood-lights-red-norm";
var c_turn_to_launch_hdg = "controls/turn-to-launch-hdg";
var c_turn_to_recvry_hdg = "controls/turn-to-recovery-hdg";
var c_turn_to_base_co = "controls/turn-to-base-course";
var mp_heading = "orientation/true-heading-deg";
var mp_pitch = "orientation/pitch-deg";
var mp_roll = "orientation/roll-deg";
# MP transmitted controls
var mp_speed = "surface-positions/flap-pos-norm";
var mp_rudder = "surface-positions/rudder-pos-norm";
var mp_network = "sim/multiplay/generic/string[0]";
var mp_message = "sim/multiplay/generic/string[2]";
var mp_tgt_hdg = "sim/multiplay/generic/float[0]";
var mp_tgt_spd = "sim/multiplay/generic/float[1]";
var mp_turn_radius = "sim/multiplay/generic/float[2]";
# Controller parameters.
var cross_course_gain = 0.2;
var cross_course_fadeout = 100.0;
var cross_course_limit = 20.0;
###############################################################################
# Manager class for one model instance.
var Manager = {};
##################################################
Manager.new = func (player = nil, carrier_name = nil, callsign = nil) {
# e.g. carrier_name = 'Clemenceau'.
printf("Manager.new(): player=%s carrier_name=%s callsign=%s",
player, carrier_name, callsign);
if (g_auto_attach.getValue()) {
# Look for matching ai scenario and enable it. We don't care if it is
# already enabled - FGAIManager::loadScenarioCommand() will know to do
# nothing.
printf("Checking ai scenarios...");
foreach (var n; props.globals.getNode("sim/ai/scenarios", 1).getChildren("scenario")) {
var n_carrier_name = n.getValue("carrier/name");
var n_scenario_id = n.getValue("id"); # e.g. clemenceau_demo.
if (n_carrier_name != nil) {
if (0) {
printf(" %s: id=%s n_carrier_name=%s",
n.getPath(),
n_scenario_id,
n_carrier_name
);
}
if (n_carrier_name == carrier_name) {
printf("calling load-scenario with id=%s", n_scenario_id);
var args = props.Node.new( { name: n_scenario_id});
var e = fgcommand("load-scenario", args);
printf("fgcommand('load-scenario') returned e=%s", e);
if (e) {
# We have loaded scenario.
gui.popupTip(sprintf("Have loaded AI senario: %s", n_scenario_id));
}
else {
# Error, or scenario was already loaded.
}
}
}
}
printf("Enabled scenarios are:");
foreach (var n; props.globals.getNode("sim/ai", 1).getChildren("scenario")) {
printf(" %s", n.getValue());
}
}
var obj = { parents : [Manager],
rplayer : player,
carrier_name : carrier_name,
carrier : nil,
accept_callsign : callsign,
callsign_listener : nil,
FREEZE_DIST : 400.0,
comms : nil,
message : nil,
loopid : 0 };
var carriers = props.globals.getNode("/ai/models").getChildren("carrier");
foreach(var c; carriers) {
if (c.getNode("name").getValue() == carrier_name) {
obj.carrier = c;
printf("Initializing carrier_name=%s player.getPath()=%s",
carrier_name, player.getPath());
obj.callsign_listener =
setlistener(callsign,
func { print("Callsign update");
obj.start(); });
MPCarriersNW.Manager_instances[player.getIndex()] = obj;
obj.start();
return obj;
}
}
printf("Failed to find carrier_name=%s. The relevant carrier AI scenario must be active", carrier_name);
return nil;
}
##################################################
Manager.is_valid = func {
return ((me.rplayer.getNode("valid") != nil) and
me.rplayer.getNode("valid").getValue() and
(me.rplayer.getNode("callsign") != nil));
}
##################################################
Manager.is_active = func {
var a = me.is_valid();
var b = (me.rplayer.getNode("callsign") != nil);
# FIXME: Sometimes the cmp() call here gets an invalid argument.
var c = (cmp(me.rplayer.getNode("callsign").getValue(), me.accept_callsign.getValue()) == 0);
if (g_auto_attach.getValue()) {
# Always attach, regardless of callsign.
c = 1;
}
return (a and b and c);
}
##################################################
Manager.set_property = func (path, value) {
if (!me.is_valid() or !me.is_active()) return;
me.carrier.getNode(path).setValue(value);
}
##################################################
Manager.update = func {
var aircraft_pos = geo.aircraft_position();
var carrier_pos = geo.Coord.new(aircraft_pos);
if (!me.is_valid()) {
# This carrier player is not valid anymore.
if (0) printf("calling me.die() because !me.is_valid()");
me.die();
return;
}
if (!me.is_active()) {
# This carrier player is not the chosen one. Go idle.
if (0) printf("calling me.stop(). me.is_valid()=%s me.rplayer.getNode('callsign').getValue()=%s me.accept_callsign.getValue()=%s",
me.is_valid(),
me.rplayer.getNode('callsign').getValue(),
me.accept_callsign.getValue()
);
me.stop();
return;
}
# LSO comms
var message = me.rplayer.getNode(mp_message).getValue();
if (message != ""){
if (message != me.message){
setprop("/sim/messages/approach", message);
}
me.message = message;
}
if (!g_latch_always.getValue())
{
# carrier_pos.set_latlon(me.carrier.getNode(lat).getValue(),
# me.carrier.getNode(lon).getValue(),
# me.carrier.getNode(alt).getValue());
carrier_pos.set_xyz(me.carrier.getNode(x).getValue(),
me.carrier.getNode(y).getValue(),
me.carrier.getNode(z).getValue());
# Compute the position and orientation error.
var rplayer_pos = geo.Coord.new(carrier_pos);
# rplayer_pos.set_latlon(me.rplayer.getNode(lat).getValue(),
# me.rplayer.getNode(lon).getValue(),
# me.rplayer.getNode(alt).getValue());
rplayer_pos.set_xyz(me.rplayer.getNode(x).getValue(),
me.rplayer.getNode(y).getValue(),
me.rplayer.getNode(z).getValue());
}
var master_course = normalize_course(me.rplayer.getNode(mp_heading).getValue());
var master_speed = me.rplayer.getNode(mp_speed).getValue();
var bearing_to_master = normalize_course(carrier_pos.course_to(rplayer_pos));
var distance_to_master = carrier_pos.direct_distance_to(rplayer_pos);
var v = D2R * normalize_course(bearing_to_master - master_course);
var cross_track_error = distance_to_master * math.sin(v);
var along_track_error = distance_to_master * math.cos(v);
var master_tgt_hdg = as_num(me.rplayer.getNode(mp_tgt_hdg).getValue(),
me.rplayer.getNode(mp_heading).getValue());
var master_tgt_spd = as_num(me.rplayer.getNode(mp_tgt_spd).getValue(),
me.rplayer.getNode(mp_speed).getValue());
var master_turn_radius = as_num(me.rplayer.getNode(mp_turn_radius).getValue());
var diff = master_course - master_tgt_hdg;
if (diff > 180)
diff -= 360;
elsif (diff < -180)
diff += 360;
me.carrier.getNode("mp-control/ai-mp-course-delta", 1).setValue(diff);
if ( diff < -1.0 or diff > 1.0){
me.carrier.getNode("mp-control/ai-mp-course-delta-type", 1).setValue("major");
# major course alteration - we'll just use target heading from
# master until it's nearly complete
# print("major turn" , diff);
var set_course = master_tgt_hdg ;
var correction = 0;
if (diff < 0){
correction = -cross_track_error * M2FT;
# print("stbd turn ", correction);
} elsif (diff > 0){
correction = cross_track_error * M2FT;
# print("port turn ", correction);
} else {
correction = 0;
# print("no turn ", correction);
}
me.carrier.getNode("controls/turn-radius-ft", 1).setValue(master_turn_radius + correction);
} else {
# Use Controller.
me.carrier.getNode("mp-control/ai-mp-course-delta-type", 1).setValue("minor");
me.carrier.getNode("controls/turn-radius-ft", 1).setValue(master_turn_radius);
var set_course =
normalize_course(180.0/math.pi *
(math.abs(cross_track_error) < cross_course_fadeout ?
math.pow
(math.abs(cross_track_error/cross_course_fadeout),2) :
1.0) *
math.atan2(cross_course_gain * cross_track_error,
along_track_error) +
master_course);
# Limit the course to +/-cross_course_limit degrees off the master's course.
if (set_course - master_course > cross_course_limit) {
set_course = normalize_course(master_course + cross_course_limit);
} else if (set_course - master_course < -cross_course_limit) {
set_course = normalize_course(master_course - cross_course_limit);
}
}
var spd_diff = master_speed - master_tgt_spd;
me.carrier.getNode("mp-control/ai-mp-speed-delta", 1).setValue(spd_diff);
if ( spd_diff < -1 or spd_diff > 1){
# major speed alteration - we'll just use target speed from
# master until it's nearly complete
me.carrier.getNode("mp-control/ai-mp-speed-delta-type", 1).setValue("major");
var set_speed = master_tgt_spd +
0.01 * along_track_error;
if (set_speed > master_tgt_spd + 5.0) set_speed = master_tgt_spd + 5.0;
if (set_speed < master_tgt_spd - 5.0) set_speed = master_tgt_spd - 5.0;
} else {
me.carrier.getNode("mp-control/ai-mp-speed-delta-type", 1).setValue("minor");
var set_speed = master_speed +
0.01 * along_track_error;
if (set_speed > master_speed + 5.0) set_speed = master_speed + 5.0;
if (set_speed < master_speed - 5.0) set_speed = master_speed - 5.0;
}
# publish controller settings to /ai/models/carrier[]/mp-control/.
#
me.carrier.getNode("mp-control/bearing-to-master-rel-deg", 1).setValue(v);
me.carrier.getNode("mp-control/bearing-to-master-deg", 1).setValue(bearing_to_master);
me.carrier.getNode("mp-control/distance-to-master-m", 1).setValue(distance_to_master);
me.carrier.getNode("mp-control/cross-track-error-m", 1).setValue(cross_track_error);
me.carrier.getNode("mp-control/along-track-error-m", 1).setValue(along_track_error);
me.carrier.getNode("mp-control/freeze-distance", 1).setValue(me.FREEZE_DIST);
me.carrier.getNode("mp-control/master-speed-kts", 1).setValue(master_speed);
me.carrier.getNode("mp-control/master-course-deg", 1).setValue(master_course);
me.carrier.getNode("mp-control/set-speed-kts", 1).setValue(set_speed);
me.carrier.getNode("mp-control/set-course-deg", 1).setValue(set_course);
me.carrier.getNode("mp-control/tgt-hdg-deg", 1).setValue(sprintf ( "%03.1d", master_tgt_hdg));
me.carrier.getNode("mp-control/tgt-spd-kts", 1).setValue(master_tgt_spd);
me.carrier.getNode("mp-control/turn-radius-ft", 1).setValue(master_turn_radius);
# We latch ai and mp carrier positions if user aircraft is more than
# me.FREEZE_DIST away from ai carrier, or if we are in replay mode.
#
var distance_aircraft_carrier = aircraft_pos.direct_distance_to(carrier_pos);
var in_replay = props.globals.getValue("/sim/replay/replay-state");
var do_latch = (distance_aircraft_carrier > me.FREEZE_DIST or in_replay);
var distance_ai_to_mpcarrier = rplayer_pos.direct_distance_to(carrier_pos);
me.carrier.getNode("mp-control/aircraft-ai-distance", 1).setValue(distance_aircraft_carrier);
me.carrier.getNode("mp-control/ai-mp-latched", do_latch);
if (g_latch_always.getValue()) {
# Tell C++ to copy MP position/orientation directly on to AI
# position/orientation, every frame.
#
# Set /ai/models/multiplayer[]/ai-latch to "/ai/models/carrier[]".
#
# Set /ai/models/carrier[]/ai-latch to "/ai/models/multiplayer[]".
me.rplayer.getNode("ai-latch", 1).setValue(me.carrier.getPath());
me.carrier.getNode("ai-latch", 1).setValue(me.rplayer.getPath());
}
elsif (do_latch) {
# Latch the local AI carrier to the remote player's
# location only when the local player is far enough away not
# to suffer side effects.
me.carrier.getNode(lat).setValue(me.rplayer.getNode(lat).getValue());
me.carrier.getNode(lon).setValue(me.rplayer.getNode(lon).getValue());
me.carrier.getNode(alt).setValue(me.rplayer.getNode(alt).getValue());
me.carrier.getNode(c_heading).
setValue(master_course);
me.carrier.getNode(c_pitch).
setValue(me.rplayer.getNode(mp_pitch).getValue());
me.carrier.getNode(c_roll).
setValue(me.rplayer.getNode(mp_roll).getValue());
# Accelerate the AI carrier to the right speed.
me.carrier.getNode(c_control_speed).
setValue(master_speed);
} else {
# Player on deck. Use the speed controller.
me.carrier.getNode(c_control_speed).
setValue(set_speed);
}
# Always set these commands.
me.carrier.getNode(c_control_course).
setValue(set_course);
# Switch off AI control for the carrier if available.
if (me.carrier.getNode(c_control_mp_ctrl) != nil)
me.carrier.getNode(c_control_mp_ctrl).setBoolValue(1);
}
##################################################
Manager.stop = func {
me.loopid += 1;
printf("Manager.stop() me.carrier_name=%s me.rplayer.getPath()=%s",
me.carrier_name, me.rplayer.getPath());
# Reenable AI control.
if (me.carrier.getNode(c_control_mp_ctrl) != nil)
me.carrier.getNode(c_control_mp_ctrl).setBoolValue(0);
me.rplayer.getNode("ai-latch").setValue("");
me.carrier.getNode("ai-latch").setValue("");
}
##################################################
Manager.die = func {
if (me.callsign_listener == nil) return;
delete(MPCarriersNW.Manager_instances, me.rplayer.getIndex());
me.loopid += 1;
# Delay the reactivation of AI control.
settimer(func {
if (me.carrier.getNode(c_control_mp_ctrl) != nil)
me.carrier.getNode(c_control_mp_ctrl).setBoolValue(0);
}, 5.0);
removelistener(me.callsign_listener);
me.callsign_listener = nil;
printf("Manager.die(): me.carrier_name=%s me.rplayer.getPath()=%s",
me.carrier_name, me.rplayer.getPath());
}
##################################################
Manager.start = func {
me.loopid += 1;
printf("Manager.start(): me.carrier_name=%s me.rplayer.getPath()=%s",
me.carrier_name, me.rplayer.getPath());
me._loop_(me.loopid);
}
##################################################
Manager._loop_ = func(id) {
id == me.loopid or return;
me.update();
settimer(func { me._loop_(id); }, 0);
}
###############################################################################
###############################################################################
#################################################################
var normalize_course = func(c) {
while (c < 0) c += 360;
while (c >= 360) c -= 360;
return c;
}
#################################################################
# Return a hash containing all nearby carrier players
# indexed on MP-carrier type
var find_carrier_players = func {
var res = {};
foreach (var c; keys(MPCarriersNW.Manager_instances)) {
if (MPCarriersNW.Manager_instances[c].is_valid()) {
var type = MPCarriersNW.Manager_instances[c].carrier_name;
var pilot = MPCarriersNW.Manager_instances[c].rplayer;
if (!contains(res, type)) {
res[type] = [pilot.getNode("callsign").getValue()];
} else {
append(res[type], pilot.getNode("callsign").getValue());
}
}
}
# debug.dump(res);
return res;
}
###############################################################################
# MPCarrier selection dialog.
var CARRIER_DLG = 0;
var carrier_dialog = {};
############################################################
carrier_dialog.init = func (x = nil, y = nil) {
me.x = x;
me.y = y;
me.bg = [0, 0, 0, 0.3]; # background color
me.fg = [[1.0, 1.0, 1.0, 1.0]];
#
# "private"
me.title = "MPCarrier";
me.basenode = props.globals.getNode("/sim/mp-carriers", 1);
me.dialog = nil;
me.namenode = props.Node.new({"dialog-name" : me.title });
me.listeners = [];
me.players = {};
me.carriers = { "Nimitz" : "nimitz-callsign",
"Eisenhower" : "eisenhower-callsign",
"Truman" : "truman-callsign",
"Foch" : "foch-callsign",
"Clemenceau" : "clemenceau-callsign",
"Vinson" : "vinson-callsign"
"Kuznetsov" : "kuznetsov-callsign",
"Liaoning" : "liaoning-callsign",
"Sanantonio" : "sanantonio-callsign"};
}
############################################################
carrier_dialog.create = func {
if (me.dialog != nil)
me.close();
me.dialog = gui.Widget.new();
gui.dialog[me.title] = me.dialog;
me.dialog.set("name", me.title);
if (me.x != nil)
me.dialog.set("x", me.x);
if (me.y != nil)
me.dialog.set("y", me.y);
me.dialog.set("layout", "vbox");
me.dialog.set("default-padding", 0);
var titlebar = me.dialog.addChild("group");
titlebar.set("layout", "hbox");
titlebar.addChild("empty").set("stretch", 1);
titlebar.addChild("text").set("label", "MPCarriers online");
var w = titlebar.addChild("button");
w.set("pref-width", 16);
w.set("pref-height", 16);
w.set("legend", "");
w.set("default", 0);
w.set("key", "esc");
w.setBinding("nasal", "MPCarriers.carrier_dialog.destroy(); ");
w.setBinding("dialog-close");
me.dialog.addChild("hrule");
var content = me.dialog.addChild("group");
content.set("layout", "vbox");
content.set("halign", "center");
content.set("default-padding", 5);
# Generate the dialog contents.
var players_old = me.players;
me.players = find_carrier_players();
foreach (var type; keys(me.carriers)) {
var selected = me.basenode.getNode(me.carriers[type], 1).getValue();
var tmpbase = me.basenode.getNode(type, 1);
if (contains(me.players, type)) {
var i = 0;
foreach (var p; me.players[type]) {
var tmp = tmpbase.getNode("b[" ~ i ~ "]", 1);
tmp.setBoolValue(streq(selected, p));
var w = content.addChild("checkbox");
w.node.setValues({"label" : p ~ " (" ~ type ~ ")",
"halign" : "left",
"property" : tmp.getPath()});
w.setBinding
("nasal",
"MPCarriers.carrier_dialog.select_action(" ~
"\"" ~ type ~ "\", " ~ i ~ ");");
i = i + 1;
}
if (size(me.players[type]) == 1
and (
!contains(players_old, type)
or size(players_old[type]) == 0
or g_auto_attach.getValue()
)) {
# Carrier has just appeared, and there is no other carrier of
# the same type, so attach our AI carrier to it.
printf("carrier_dialog.create(): auto-selecting type=%s", type);
me.select_action(type, 0);
}
}
}
me.dialog.addChild("hrule");
# Display the dialog.
fgcommand("dialog-new", me.dialog.prop());
fgcommand("dialog-show", me.namenode);
}
############################################################
carrier_dialog.close = func {
if (me.dialog != nil) {
me.x = me.dialog.prop().getNode("x", 1).getValue();
me.y = me.dialog.prop().getNode("y", 1).getValue();
}
fgcommand("dialog-close", me.namenode);
}
############################################################
carrier_dialog.destroy = func {
CARRIER_DLG = 0;
me.close();
foreach(var l; me.listeners)
removelistener(l);
delete(gui.dialog, "\"" ~ me.title ~ "\"");
}
############################################################
carrier_dialog.show = func {
# print("Showing MPCarriers dialog!");
if (!CARRIER_DLG) {
CARRIER_DLG = int(getprop("/sim/time/elapsed-sec"));
me.init();
me.create();
me._update_(CARRIER_DLG);
}
}
############################################################
carrier_dialog._redraw_ = func {
if (me.dialog != nil) {
me.close();
me.create();
}
}
############################################################
carrier_dialog._update_ = func (id) {
if (CARRIER_DLG != id) return;
me._redraw_();
settimer(func { me._update_(id); }, 4.1);
}
############################################################
carrier_dialog.select_action = func (type, n) {
var base = me.basenode.getNode(type);
var selected = me.basenode.getNode(me.carriers[type]).getValue();
var bs = base.getChildren();
# Assumption: There are two true b:s or none. The one not matching selected
# is the new selection.
var i = 0;
me.basenode.getNode(me.carriers[type], 1).setValue("");
foreach (var b; bs) {
if (!b.getValue() and (i == n)) {
b.setValue(1);
me.basenode.getNode(me.carriers[type]).setValue
(me.players[type][i]);
} else {
b.setValue(0);
}
i = i + 1;
}
me._redraw_();
}
###############################################################################
var as_num = func (val, default=0.0) {
return (typeof(val) == "scalar") ? val : default;
}
###############################################################################
# Overall initialization. Should only take place when the MPCarrier module is
# being loaded.
# Load the MPCarrier MP network.
if (!contains(globals, "MPCarriersNW")) {
var base = "Aircraft/MPCarrier/Systems/mp-network.nas";
io.load_nasal(resolvepath(base), "MPCarriersNW");
MPCarriersNW.mp_network_init(0);
}