Initial commit
This commit is contained in:
parent
c39b57aeb5
commit
c144a89435
74
README.md
74
README.md
@ -1 +1,73 @@
|
||||
# flightgear-addon-aerotow-everywhere
|
||||
# "Aerotow Everywhere" - FlightGear add-on
|
||||
|
||||
This is an add-on designed to include an AI aircraft that will be able to tow a glider. The main idea is to be able to do this at any airport where you start with your glider.
|
||||
|
||||
## Instalation
|
||||
|
||||
This add-on creates a flight plan in real-time based on the airport you are at. Unfortunately this causes a problem which you will have to solve manually by following the instructions below.
|
||||
Namely, the flight plan for AI aircraft, by default, must be stored in the `$FG_ROOT/AI/FlightPlanes/` directory. But for security reasons, Nasal scripts cannot save files to that directory, but can save e.g. to `$FG_HOME/Export/`. Therefore, the run-time created flight plan will just be saved to `$FG_HOME/Export/Addons/org.flightgear.addons.Aerotow/AI/FlightPlanes/`. The problem is that FlightGear does not know that it is supposed to look for the flight plan file in this location as well, so you have to tell it manually by using the `--data` command line option.
|
||||
|
||||
`$FG_HOME` is the FlightGear home path. It differs depending on the operating system.
|
||||
Under Linux/macOS `$FG_HOME` is `/home/{username}/.fgfs/`.
|
||||
Under Windows `$FG_HOME` is `C:\Users\{username}\AppData\Roaming\flightgear.org\`.
|
||||
Where `{username}` is the name of the user logged into the operating system.
|
||||
|
||||
1. Download "Aerotow Everywhere" add-on and unzip it.
|
||||
2. In Launcher go to "Add-ons" tab. Click "Add" button by "Add-on Module folders" section and select folder with unzipped "Aerotow Everywhere" add-on directory (or add command line options: `--addon=path`), and click "Fly!". After loading the simulator, a `$FG_HOME/Export/Addons/org.flightgear.addons.Aerotow` directory should be created.
|
||||
3. Close FlightGear for now.
|
||||
4. In the file explorer of your operating system, find the directory `$FG_HOME/Export/Addons/org.flightgear.addons.Aerotow`. This path must be added by the `--data` command line option, so it will be treated as an additional FGData directory. In Launcher go to "Settings" tab and in "Additionnal Setting" type for example on the Linux system: `--data=/home/{username}/.fgfs/Export/Addons/org.flightgear.addons.Aerotow` or on the Windows: `--data=C:\Users\{username}\AppData\Roaming\flightgear.org\Export\Addons\org.flightgear.addons.Aerotow`.
|
||||
5. Run simulator again, now everything should be working.
|
||||
|
||||
## How to start?
|
||||
|
||||
Start FlightGear at any airport with your aircraft as a glider, such as ASK 21.
|
||||
|
||||
From the top menu, select "Aerotow Everywhere" -> "Call for Piper J3 Cub aircraft", "Robin DR400" or "Cessna 182" (yes, you can choose from many aircrafts). The AI aircraft will appear to your right and align to the centreline of the runway in front of you. At this time you should hook up to the aircraft, most often by pressing the `Ctrl-o` key (check help of you glider). The AI aircraft will begin to accelerate and take off.
|
||||
|
||||
## How does the AI tow aircraft fly?
|
||||
|
||||
The tow plane always takes off in front of your runway and flies along the runway for 5 km, then turns back and flies downwind for 6 km, then turns back again and flies another 6 km. During this flight it is constantly gaining altitude. Then when it has completed the entire set route it simply turns right 90 degrees and flies at a constant altitude.
|
||||
|
||||
You can disconnect from the aircraft at any time, most often by pressing the `o` key.
|
||||
|
||||
## Menu of add-on
|
||||
|
||||
This add-on add a new item to main menu named "Aerotow Everywhere" with following items:
|
||||
|
||||
1. "Call for Piper J3 Cub aircraft" - load AI tow sceneraio with Piper J3 Cub. Possible altitude to reach ~3,600 ft.
|
||||
2. "Call for Robin DR400 aircraft" - load AI tow sceneraio with Robin DR400. This aircraft has better performance and can take you to over 4,500 ft.
|
||||
3. "Call for Cessna 182 aircraft" - load AI tow sceneraio with Cessna 182. This aircraft has little bit better performance than Robin.
|
||||
4. "Disable tow aircraft" - unload AI tow sceneraio.
|
||||
5. "Help" - display help dialog.
|
||||
6. "About" - display about dialog with add-on information.
|
||||
|
||||
## Limitations
|
||||
|
||||
1. This add-on doesn't check if there are any obstacles in the aircraft's path, e.g. terrain, buildings, power lines, etc. This should be borne in mind when selecting an airport.
|
||||
|
||||
## Troubleshotting
|
||||
|
||||
1. When I select "Aerotow Everywhere" -> "Call for Piper J3 Cub aircraft", "Robin DR400" or "Cessana 182" from menu, I see "Let's fly!" message but nothing happened. The tow plane does not appear.
|
||||
|
||||
Probably you didn't include the `--data` command line option with the path where FlightGear should look for additional flight plan files for AI. Unfortunately, the simulator does not inform us that there was a problem with finding the flight plan file, so everything looks like it should work but does not.
|
||||
For fix it, in the file explorer of your operating system, find the directory `$FG_HOME/Export/Addons/org.flightgear.addons.Aerotow`. This path must be added by the `--data` command line option, so it will be treated as an additional FGData directory where FlightGear will find the flight plan. In Launcher go to "Settings" tab and in "Additionnal Setting" type (on the Linux system):
|
||||
|
||||
```
|
||||
--data=/home/{username}/.fgfs/Export/Addons/org.flightgear.addons.Aerotow
|
||||
```
|
||||
|
||||
or on the Windows:
|
||||
|
||||
```
|
||||
--data=C:\Users\{username}\AppData\Roaming\flightgear.org\Export\Addons\org.flightgear.addons.Aerotow
|
||||
```
|
||||
|
||||
Run simulator again, now everything should be working.
|
||||
|
||||
## Authors
|
||||
|
||||
- Roman "PlayeRom" Ludwicki
|
||||
|
||||
## License
|
||||
|
||||
Aerotow Everywhere is an Open Source project and it is licensed under the GNU Public License v3 (GPLv3).
|
||||
|
59
addon-config.xml
Normal file
59
addon-config.xml
Normal file
@ -0,0 +1,59 @@
|
||||
<?xml version="1.0"?>
|
||||
<!--
|
||||
#
|
||||
# Aerotow Everywhere - Add-on for FlightGear
|
||||
#
|
||||
# Written and developer by Roman Ludwicki (PlayeRom, SP-ROM)
|
||||
#
|
||||
# Copyright (C) 2022 Roman Ludwicki
|
||||
#
|
||||
# Aerotow Everywhere is an Open Source project and it is licensed
|
||||
# under the GNU Public License v3 (GPLv3)
|
||||
#
|
||||
-->
|
||||
|
||||
<!--
|
||||
use this addon-config.xml to create/set/publish your property tree
|
||||
or override default settings from defaults.xml
|
||||
|
||||
Node: this file MUST be named addon-config.xml and it MUST be in the
|
||||
root path of your addon/plugin.
|
||||
The topmost element MUST be <PropertyList>
|
||||
-->
|
||||
|
||||
<PropertyList>
|
||||
<addons>
|
||||
<by-id>
|
||||
<org.flightgear.addons.Aerotow>
|
||||
<addon-devel>
|
||||
<ai-model type="string">Cub</ai-model>
|
||||
<help-text>=== How to start? ===
|
||||
|
||||
From the top menu, select "Aerotow Everywhere" -> "Call for Piper J3 Cub aircraft", "Robin DR400" or "Cessna 182" (yes, you can choose from many aircrafts).
|
||||
The tow aircraft will appear to your right and align to the centreline of the runway in front of you. At this time you should hook up to the aircraft, most often by `Ctrl-o` key (check help of you glider). The aircraft will begin to accelerate and take off.
|
||||
|
||||
The tow plane always takes off in front of your runway and flies along the runway for 5 km, then turns back and flies downwind for 6 km, then turns back again and flies another 6 km. During this flight it is constantly gaining altitude. Then when it has completed the entire set route it simply turns right 90 degrees and flies at a constant altitude.
|
||||
|
||||
You can disconnect from the aircraft at any time, most often by pressing the `o` key.
|
||||
|
||||
=== Troubleshotting ===
|
||||
|
||||
1. When I select "Aerotow Everywhere" -> "Call for Piper J3 Cub aircraft", "Robin DR400" or "Cessna 182" from menu, I see "Let's fly!" message but nothing happened. The tow plane does not appear.
|
||||
|
||||
Probably you didn't include the `--data` command line option with the path where FlightGear should look for additional flight plan files for AI. Unfortunately, the simulator does not inform us that there was a problem with finding the flight plan file, so everything looks like it should work but does not.
|
||||
For fix it, in the file explorer of your operating system, find the directory `$FG_HOME/Export/Addons/org.flightgear.addons.Aerotow`. This path must be added by the `--data` command line option, so it will be treated as an additional FGData directory where FlightGear will find the flight plan. In Launcher go to "Settings" tab and in "Additionnal Setting" type (on the Linux system):
|
||||
|
||||
--data=/home/{username}/.fgfs/Export/Addons/org.flightgear.addons.Aerotow
|
||||
|
||||
or on the Windows:
|
||||
|
||||
--data=C:\Users\{username}\AppData\Roaming\flightgear.org\Export\Addons\org.flightgear.addons.Aerotow
|
||||
|
||||
Run simulator again, now everything should be working.
|
||||
|
||||
</help-text>
|
||||
</addon-devel>
|
||||
</org.flightgear.addons.Aerotow>
|
||||
</by-id>
|
||||
</addons>
|
||||
</PropertyList>
|
52
addon-main.nas
Normal file
52
addon-main.nas
Normal file
@ -0,0 +1,52 @@
|
||||
#
|
||||
# Aerotow Everywhere - Add-on for FlightGear
|
||||
#
|
||||
# Written and developer by Roman Ludwicki (PlayeRom, SP-ROM)
|
||||
#
|
||||
# Copyright (C) 2022 Roman Ludwicki
|
||||
#
|
||||
# Aerotow Everywhere is an Open Source project and it is licensed
|
||||
# under the GNU Public License v3 (GPLv3)
|
||||
#
|
||||
|
||||
var unload = func(addon) {
|
||||
# This function is for addon development only. It is called on addon
|
||||
# reload. The addons system will replace setlistener() and maketimer() to
|
||||
# track this resources automatically for you.
|
||||
#
|
||||
# Listeners created with setlistener() will be removed automatically for you.
|
||||
# Timers created with maketimer() will have their stop() method called
|
||||
# automatically for you. You should NOT use settimer anymore, see wiki at
|
||||
# http://wiki.flightgear.org/Nasal_library#maketimer.28.29
|
||||
#
|
||||
# Other resources should be freed by adding the corresponding code here,
|
||||
# e.g. myCanvas.del();
|
||||
}
|
||||
|
||||
var main = func(addon) {
|
||||
print("Aerotow Everywhere add-on initialized from path ", addon.basePath);
|
||||
|
||||
addon.createStorageDir();
|
||||
|
||||
# Create /AI/FlightPlans/ directory in $FG_HOME/Export/Addons/org.flightgear.addons.Aerotow/
|
||||
# User has to add the path as --data=$FG_HOME/Export/Addons/org.flightgear.addons.Aerotow
|
||||
# Then the FG will be able to read flight plan file
|
||||
var path = os.path.new(addon.storagePath ~ "/AI/FlightPlans/dummy-file.txt");
|
||||
path.create_dir();
|
||||
|
||||
loadExtraNasalFiles(addon);
|
||||
|
||||
setlistener("/addons/by-id/org.flightgear.addons.Aerotow/addon-devel/ai-model", func(n) {
|
||||
aerotow.startAerotow();
|
||||
});
|
||||
}
|
||||
|
||||
var loadExtraNasalFiles = func (addon) {
|
||||
foreach (var scriptName; ["aerotow", "messages"]) {
|
||||
var fileName = addon.basePath ~ "/" ~ scriptName ~ ".nas";
|
||||
|
||||
print("Loading Aerotown Add-on module ", fileName);
|
||||
|
||||
io.load_nasal(fileName, scriptName);
|
||||
}
|
||||
}
|
102
addon-menubar-items.xml
Normal file
102
addon-menubar-items.xml
Normal file
@ -0,0 +1,102 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
#
|
||||
# Aerotow Everywhere - Add-on for FlightGear
|
||||
#
|
||||
# Written and developer by Roman Ludwicki (PlayeRom, SP-ROM)
|
||||
#
|
||||
# Copyright (C) 2022 Roman Ludwicki
|
||||
#
|
||||
# Aerotow Everywhere is an Open Source project and it is licensed
|
||||
# under the GNU Public License v3 (GPLv3)
|
||||
#
|
||||
-->
|
||||
|
||||
<!--
|
||||
2018-12 WARNING:
|
||||
PUI menu items must have a globally unique label to make bindings work
|
||||
correctly. Bindings of all items with the same label will be triggered
|
||||
if any of them is selected from the menu.
|
||||
-->
|
||||
|
||||
<PropertyList>
|
||||
<meta>
|
||||
<file-type type="string">FlightGear add-on menu bar items</file-type>
|
||||
<format-version type="int">1</format-version>
|
||||
</meta>
|
||||
<menubar-items>
|
||||
<menu>
|
||||
<label>Aerotow Everywhere</label>
|
||||
<enabled type="bool">true</enabled>
|
||||
|
||||
<item>
|
||||
<label>Call for Piper J3 Cub aircraft</label>
|
||||
<binding>
|
||||
<command>property-assign</command>
|
||||
<property>/addons/by-id/org.flightgear.addons.Aerotow/addon-devel/ai-model</property>
|
||||
<value type="string">Cub</value>
|
||||
</binding>
|
||||
</item>
|
||||
<item>
|
||||
<label>Call for Robin DR400 aircraft</label>
|
||||
<binding>
|
||||
<command>property-assign</command>
|
||||
<property>/addons/by-id/org.flightgear.addons.Aerotow/addon-devel/ai-model</property>
|
||||
<value type="string">DR400</value>
|
||||
</binding>
|
||||
</item>
|
||||
<item>
|
||||
<label>Call for Cessna 182 aircraft</label>
|
||||
<binding>
|
||||
<command>property-assign</command>
|
||||
<property>/addons/by-id/org.flightgear.addons.Aerotow/addon-devel/ai-model</property>
|
||||
<value type="string">c182</value>
|
||||
</binding>
|
||||
</item>
|
||||
<item>
|
||||
<label>------------------</label>
|
||||
<enabled>false</enabled>
|
||||
</item>
|
||||
<item>
|
||||
<label>Disable tow aircraft</label>
|
||||
<binding>
|
||||
<command>nasal</command>
|
||||
<script><![CDATA[aerotow.stopAerotow();]]></script>
|
||||
</binding>
|
||||
</item>
|
||||
<item>
|
||||
<label>------------------</label>
|
||||
<enabled>false</enabled>
|
||||
</item>
|
||||
<item>
|
||||
<label>Help</label>
|
||||
<binding>
|
||||
<command>dialog-show</command>
|
||||
<dialog-name>help-aerotow</dialog-name>
|
||||
</binding>
|
||||
</item>
|
||||
<item>
|
||||
<label>About</label>
|
||||
<binding>
|
||||
<command>dialog-show</command>
|
||||
<dialog-name>about-aerotow</dialog-name>
|
||||
</binding>
|
||||
</item>
|
||||
|
||||
<!--
|
||||
FOR DEVELOPMENT ONLY!
|
||||
trigger reload of addon-main.nas
|
||||
1) This item should be REMOVED for release versions.
|
||||
2) Ensure the label is unique
|
||||
3) replace addon ID to match your addon
|
||||
-->
|
||||
<!-- <item>
|
||||
<label>Reload Add-on</label>
|
||||
<binding>
|
||||
<command>addon-reload</command>
|
||||
<id>org.flightgear.addons.Aerotow</id>
|
||||
</binding>
|
||||
</item> -->
|
||||
</menu>
|
||||
</menubar-items>
|
||||
</PropertyList>
|
101
addon-metadata.xml
Normal file
101
addon-metadata.xml
Normal file
@ -0,0 +1,101 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
#
|
||||
# Aerotow Everywhere - Add-on for FlightGear
|
||||
#
|
||||
# Written and developer by Roman Ludwicki (PlayeRom, SP-ROM)
|
||||
#
|
||||
# Copyright (C) 2022 Roman Ludwicki
|
||||
#
|
||||
# Aerotow Everywhere is an Open Source project and it is licensed
|
||||
# under the GNU Public License v3 (GPLv3)
|
||||
#
|
||||
-->
|
||||
|
||||
<PropertyList>
|
||||
<meta>
|
||||
<file-type type="string">FlightGear add-on metadata</file-type>
|
||||
<format-version type="int">1</format-version>
|
||||
</meta>
|
||||
|
||||
<addon>
|
||||
<identifier type="string">org.flightgear.addons.Aerotow</identifier>
|
||||
<name type="string">Aerotow Everywhere</name>
|
||||
<version type="string">1.0.0</version>
|
||||
|
||||
<authors>
|
||||
<author>
|
||||
<name type="string">Roman Ludwicki</name>
|
||||
<email type="string">kontakt@flightgear.org.pl</email>
|
||||
<url type="string">https://flightgear.org.pl</url>
|
||||
</author>
|
||||
</authors>
|
||||
|
||||
<maintainers>
|
||||
<maintainer>
|
||||
<name type="string">Roman Ludwicki</name>
|
||||
<url type="string">
|
||||
https://github.com/PlayeRom/flightgear-addon-aerotow-everywhere
|
||||
</url>
|
||||
</maintainer>
|
||||
</maintainers>
|
||||
|
||||
<short-description type="string">
|
||||
Towing aircraft for gliders at every airport.
|
||||
</short-description>
|
||||
|
||||
<long-description type="string">
|
||||
This add-on allows you to enable the towing aircraft AI scenario for gliders at any airport.
|
||||
</long-description>
|
||||
|
||||
<localized>
|
||||
<pl>
|
||||
<short-description type="string">
|
||||
Samoloty holownicze dla szybowców na każdym lotnisku.
|
||||
</short-description>
|
||||
|
||||
<long-description type="string">
|
||||
Ten dodatek umożliwia włączenie scenariusza SI z samolotem holowniczym dla szybowców na dowolnym lotnisku.
|
||||
</long-description>
|
||||
</pl>
|
||||
</localized>
|
||||
|
||||
<license>
|
||||
<designation type="string">GNU GPL version 3 or later</designation>
|
||||
|
||||
<file type="string">
|
||||
<!-- Use a relative, slash-separated path to a file under the add-on
|
||||
base directory that contains the add-on license text. -->
|
||||
</file>
|
||||
|
||||
<url type="string">https://www.gnu.org/licenses/gpl-3.0.html</url>
|
||||
</license>
|
||||
|
||||
<min-FG-version type="string">2020.4.0</min-FG-version>
|
||||
<max-FG-version type="string">none</max-FG-version>
|
||||
|
||||
<urls>
|
||||
<home-page type="string">
|
||||
https://github.com/PlayeRom/flightgear-addon-aerotow-everywhere
|
||||
</home-page>
|
||||
|
||||
<download type="string">
|
||||
https://github.com/PlayeRom/flightgear-addon-aerotow-everywhere
|
||||
</download>
|
||||
|
||||
<support type="string">
|
||||
https://github.com/PlayeRom/flightgear-addon-aerotow-everywhere
|
||||
</support>
|
||||
|
||||
<code-repository type="string">
|
||||
https://github.com/PlayeRom/flightgear-addon-aerotow-everywhere
|
||||
</code-repository>
|
||||
</urls>
|
||||
|
||||
<tags>
|
||||
<tag type="string">aerotow</tag>
|
||||
<tag type="string">AI</tag>
|
||||
<tag type="string">scenario</tag>
|
||||
</tags>
|
||||
</addon>
|
||||
</PropertyList>
|
473
aerotow.nas
Normal file
473
aerotow.nas
Normal file
@ -0,0 +1,473 @@
|
||||
#
|
||||
# Aerotow Everywhere - Add-on for FlightGear
|
||||
#
|
||||
# Written and developer by Roman Ludwicki (PlayeRom, SP-ROM)
|
||||
#
|
||||
# Copyright (C) 2022 Roman Ludwicki
|
||||
#
|
||||
# Aerotow Everywhere is an Open Source project and it is licensed
|
||||
# under the GNU Public License v3 (GPLv3)
|
||||
#
|
||||
|
||||
#
|
||||
# Constants
|
||||
#
|
||||
var addon = addons.getAddon("org.flightgear.addons.Aerotow");
|
||||
var FILENAME_SCENARIO = "aerotown-addon.xml";
|
||||
var FILENAME_FLIGHTPLAN = "aerotown-addon-flightplan.xml";
|
||||
var PATH_SCENARIO = addon.storagePath ~ "/" ~ FILENAME_SCENARIO;
|
||||
var PATH_FLIGHTPLAN = addon.storagePath ~ "/AI/FlightPlans/" ~ FILENAME_FLIGHTPLAN;
|
||||
var SCENARIO_ID = "aerotow_addon";
|
||||
var SCENARIO_NAME = "Aerotow Add-on";
|
||||
var SCENARIO_DESC = "This scenario starts the towing plane at the airport where the pilot with the glider is located. Use Ctrl-o to hook the plane.";
|
||||
var METER_TO_FEET = 3.2808399;
|
||||
var MAX_RWY_LENGTH = 200; # Sum of distance in Rolling waypoints in meters
|
||||
|
||||
#
|
||||
# Global variables
|
||||
#
|
||||
var g_wptCount = 0;
|
||||
var g_isScenarioLoaded = 0;
|
||||
var g_fpFileHandler = nil; # Handler for wrire flight plan to file
|
||||
var g_coord = nil; # Coordinates for flight plan
|
||||
var g_heading = nil; # AI plane heading
|
||||
var g_altitude = nil; # AI plane altitude
|
||||
|
||||
#
|
||||
# Main function to prepare AI scenario and run it.
|
||||
#
|
||||
# Return 1 on successful, otherwise 0.
|
||||
#
|
||||
var startAerotow = func () {
|
||||
var args = props.Node.new({ "name": SCENARIO_ID });
|
||||
if (g_isScenarioLoaded) {
|
||||
fgcommand("unload-scenario", args);
|
||||
}
|
||||
|
||||
generateScenarioXml();
|
||||
|
||||
if (!generateFlightPlanXml()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (fgcommand("load-scenario", args)) {
|
||||
g_isScenarioLoaded = 1;
|
||||
messages.displayOk("Let's fly!");
|
||||
return 1;
|
||||
}
|
||||
|
||||
messages.displayError("Tow failed!");
|
||||
return 0;
|
||||
}
|
||||
|
||||
#
|
||||
# Function for unload our AI scenario.
|
||||
#
|
||||
# Return 1 on successful, otherwise 0.
|
||||
#
|
||||
var stopAerotow = func () {
|
||||
if (g_isScenarioLoaded) {
|
||||
var args = props.Node.new({ "name": SCENARIO_ID });
|
||||
if (fgcommand("unload-scenario", args)) {
|
||||
g_isScenarioLoaded = 0;
|
||||
|
||||
messages.displayOk("Aerotown disabled");
|
||||
return 1;
|
||||
}
|
||||
|
||||
messages.displayError("Aerotown disable failed");
|
||||
return 0;
|
||||
}
|
||||
|
||||
messages.displayOk("Aerotown already disabled");
|
||||
return 1
|
||||
}
|
||||
|
||||
#
|
||||
# Generate the XML file with the AI scenario.
|
||||
# The file will be stored to $FG_HOME/Export/aerotown-addon.xml.
|
||||
#
|
||||
var generateScenarioXml = func () {
|
||||
var scenarioXml = {
|
||||
"PropertyList": {
|
||||
"scenario": {
|
||||
"name": SCENARIO_NAME,
|
||||
"description": SCENARIO_DESC,
|
||||
"entry": {
|
||||
"callsign": "FG-TOW",
|
||||
"type": "aircraft",
|
||||
"class": "aerotow-dragger",
|
||||
"model": "Aircraft/Cub/Models/Cub-ai.xml", # default Cub
|
||||
"flightplan": FILENAME_FLIGHTPLAN,
|
||||
"repeat": 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var aiModel = getSelectedAircraft();
|
||||
if (aiModel == "DR400") {
|
||||
scenarioXml.PropertyList.scenario.entry.model = "Aircraft/DR400/Models/dr400-ai.xml";
|
||||
}
|
||||
else if (aiModel == "c182") {
|
||||
scenarioXml.PropertyList.scenario.entry.model = "Aircraft/c182/Models/c182-ai.xml";
|
||||
}
|
||||
|
||||
var node = props.Node.new(scenarioXml);
|
||||
io.writexml(PATH_SCENARIO, node);
|
||||
|
||||
addScenarioToPropertyList();
|
||||
}
|
||||
|
||||
#
|
||||
# Add our new scenario to the "/sim/ai/scenarios" property list
|
||||
# so that FlightGear will be able to load it by "load-scenario" command.
|
||||
#
|
||||
var addScenarioToPropertyList = func () {
|
||||
if (!isScenarioAdded()) {
|
||||
var scenarioData = {
|
||||
"name": SCENARIO_NAME,
|
||||
"id": SCENARIO_ID,
|
||||
"description": SCENARIO_DESC,
|
||||
"path": PATH_SCENARIO,
|
||||
};
|
||||
|
||||
props.globals.getNode("/sim/ai/scenarios").addChild("scenario").setValues(scenarioData);
|
||||
}
|
||||
}
|
||||
|
||||
#
|
||||
# Return 1 if scenario is already added to "/sim/ai/scenarios" property list, otherwise return 0.
|
||||
#
|
||||
var isScenarioAdded = func () {
|
||||
foreach (var scenario; props.globals.getNode("/sim/ai/scenarios").getChildren("scenario")) {
|
||||
var id = scenario.getChild("id");
|
||||
if (id != nil and id.getValue() == SCENARIO_ID) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
#
|
||||
# Return name of selected aircraft. Possible values: "Cub", "DR400", "c182".
|
||||
#
|
||||
var getSelectedAircraft = func () {
|
||||
return getprop("/addons/by-id/org.flightgear.addons.Aerotow/addon-devel/ai-model") or "Cub";
|
||||
}
|
||||
|
||||
#
|
||||
# Generate the XML file with the flight plane for our plane for AI scenario.
|
||||
# The file will be stored to $FG_HOME/Export/aerotown-addon-flightplan.xml.
|
||||
#
|
||||
# Return 1 on successful, otherwise 0.
|
||||
#
|
||||
var generateFlightPlanXml = func () {
|
||||
g_wptCount = 0;
|
||||
|
||||
var icao = getprop("/sim/airport/closest-airport-id");
|
||||
if (icao == nil) {
|
||||
messages.displayError("Airport code cannot be obtained.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
var runwayName = getprop("/sim/atc/runway");
|
||||
if (runwayName == nil) {
|
||||
messages.displayError("Runway name cannot be obtained.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
var airport = airportinfo(icao);
|
||||
var runway = airport.runways[runwayName];
|
||||
|
||||
if (runway.length < MAX_RWY_LENGTH) {
|
||||
messages.displayError(
|
||||
"This runway is too short. Please choose a longer one then " ~ MAX_RWY_LENGTH ~ " m "
|
||||
~ "(" ~ math.round(MAX_RWY_LENGTH * METER_TO_FEET) ~ " ft)."
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
|
||||
g_fpFileHandler = io.open(PATH_FLIGHTPLAN, "w");
|
||||
io.write(
|
||||
g_fpFileHandler,
|
||||
"<?xml version=\"1.0\"?>\n\n" ~
|
||||
"<!-- This file is generated automatically by the Aerotow Everywhere add-on -->\n\n" ~
|
||||
"<PropertyList>\n" ~
|
||||
" <flightplan>\n"
|
||||
);
|
||||
|
||||
var perf = getAircraftPerformance();
|
||||
|
||||
initAircraftVariable(airport, runway, 1);
|
||||
|
||||
# Start at 2 o'clock from the glider and line up with the runway
|
||||
# Inital ktas must be >= 1.0
|
||||
addWptGround({"hdgChange": 60, "dist": 25}, {"altChange": 0, "ktas": 5});
|
||||
|
||||
# Reset coord and heading
|
||||
initAircraftVariable(airport, runway, 0);
|
||||
|
||||
var gliderOffsetM = getGliderOffsetRunwayThreshold(runway);
|
||||
|
||||
# Rolling
|
||||
addWptGround({"hdgChange": 0, "dist": 30 + gliderOffsetM}, {"altChange": 0, "ktas": 2.5});
|
||||
addWptGround({"hdgChange": 0, "dist": 5}, {"altChange": 0, "ktas": 5});
|
||||
addWptGround({"hdgChange": 0, "dist": 50}, {"altChange": 0, "ktas": perf.speed / 4});
|
||||
addWptGround({"hdgChange": 0, "dist": 50}, {"altChange": 0, "ktas": perf.speed / 1.8});
|
||||
addWptGround({"hdgChange": 0, "dist": 50}, {"altChange": 0, "ktas": perf.speed});
|
||||
|
||||
# Takeof
|
||||
addWptAir({"hdgChange": 0, "dist": 20}, {"altChange": 2, "ktas": perf.speed * 1.05});
|
||||
addWptAir({"hdgChange": 0, "dist": 20}, {"altChange": 20, "ktas": perf.speed * 1.075});
|
||||
addWptAir({"hdgChange": 0, "dist": 100}, {"altChange": 240, "ktas": perf.speed * 1.1});
|
||||
|
||||
# Circle around airport
|
||||
addWptAir({"hdgChange": 0, "dist": 500}, {"altChange": perf.vs * 1.2, "ktas": perf.speed});
|
||||
addWptAir({"hdgChange": 0, "dist": 500}, {"altChange": perf.vs * 1.2, "ktas": perf.speed});
|
||||
addWptAir({"hdgChange": 0, "dist": 1000}, {"altChange": perf.vs * 1.5, "ktas": perf.speed * 1.025});
|
||||
addWptAir({"hdgChange": 0, "dist": 1000}, {"altChange": perf.vs, "ktas": perf.speed * 1.05});
|
||||
addWptAir({"hdgChange": 0, "dist": 1000}, {"altChange": perf.vs, "ktas": perf.speed * 1.075});
|
||||
addWptAir({"hdgChange": 0, "dist": 1000}, {"altChange": perf.vs, "ktas": perf.speed * 1.1});
|
||||
addWptAir({"hdgChange": -90, "dist": 1000}, {"altChange": perf.vs, "ktas": perf.speed * 1.125}); # crosswind leg
|
||||
addWptAir({"hdgChange": -90, "dist": 1000}, {"altChange": perf.vs, "ktas": perf.speed * 1.15}); # downwind leg
|
||||
addWptAir({"hdgChange": 0, "dist": 1000}, {"altChange": perf.vs, "ktas": perf.speed * 1.175});
|
||||
addWptAir({"hdgChange": 0, "dist": 1000}, {"altChange": perf.vs, "ktas": perf.speed * 1.2});
|
||||
addWptAir({"hdgChange": 0, "dist": 1000}, {"altChange": perf.vs, "ktas": perf.speed * 1.225});
|
||||
addWptAir({"hdgChange": 0, "dist": 1000}, {"altChange": perf.vs, "ktas": perf.speed * 1.25});
|
||||
addWptAir({"hdgChange": 0, "dist": 1000}, {"altChange": perf.vs, "ktas": perf.speed * 1.275});
|
||||
addWptAir({"hdgChange": -90, "dist": 500}, {"altChange": perf.vs / 2, "ktas": perf.speed * 1.25}); # base leg
|
||||
addWptAir({"hdgChange": 0, "dist": 500}, {"altChange": perf.vs / 2, "ktas": perf.speed * 1.275});
|
||||
addWptAir({"hdgChange": 0, "dist": 500}, {"altChange": perf.vs / 2, "ktas": perf.speed * 1.3});
|
||||
addWptAir({"hdgChange": -90, "dist": 1000}, {"altChange": perf.vs, "ktas": perf.speed * 1.3}); # final leg
|
||||
addWptAir({"hdgChange": 0, "dist": 1000}, {"altChange": perf.vs, "ktas": perf.speed * 1.325});
|
||||
addWptAir({"hdgChange": 0, "dist": 1000}, {"altChange": perf.vs, "ktas": perf.speed * 1.35});
|
||||
addWptAir({"hdgChange": 0, "dist": 1000}, {"altChange": perf.vs, "ktas": perf.speed * 1.375});
|
||||
addWptAir({"hdgChange": 0, "dist": 1000}, {"altChange": perf.vs, "ktas": perf.speed * 1.4});
|
||||
addWptAir({"hdgChange": 0, "dist": 1000}, {"altChange": perf.vs, "ktas": perf.speed * 1.425});
|
||||
|
||||
addWptEnd();
|
||||
|
||||
io.write(
|
||||
g_fpFileHandler,
|
||||
" </flightplan>\n" ~
|
||||
"</PropertyList>\n\n"
|
||||
);
|
||||
io.close(g_fpFileHandler);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
#
|
||||
# Return hash with "vs" and "speed".
|
||||
#
|
||||
var getAircraftPerformance = func () {
|
||||
# Cub
|
||||
# Cruise Speed 61 kt
|
||||
# Max Speed 106 kt
|
||||
# Approach speed 44-52 kt
|
||||
# Stall speed 33 kt
|
||||
|
||||
# Robin DR 400
|
||||
# Cruise Speed 134 kt
|
||||
# Max speeed 166 kt
|
||||
# Stall speed 51 kt
|
||||
# Rate of climb: 825 ft/min
|
||||
|
||||
# Cessna 182
|
||||
# Cruise Speed 145 kt
|
||||
# Max speeed 175 kt
|
||||
# Stall speed 50 kt
|
||||
# Best climb: 924 ft/min
|
||||
|
||||
var aiModel = getSelectedAircraft();
|
||||
if (aiModel == "DR400") {
|
||||
return {
|
||||
"vs": 285, # ft per 1000 m
|
||||
"speed": 70,
|
||||
};
|
||||
}
|
||||
|
||||
if (aiModel == "c182") {
|
||||
return {
|
||||
"vs": 295, # ft per 1000 m
|
||||
"speed": 75,
|
||||
};
|
||||
}
|
||||
|
||||
# Cub
|
||||
return {
|
||||
"vs": 200, # ft per 1000 m
|
||||
"speed": 55,
|
||||
};
|
||||
}
|
||||
|
||||
#
|
||||
# Initialize AI aircraft variable
|
||||
#
|
||||
# isGliderPos - Pass 1 for set AI aircraft's coordinates as glider position, 0 set coordinates as runway threshold.
|
||||
# airport - Object from airportinfo().
|
||||
# runway - Object of runway from which the glider start.
|
||||
#
|
||||
var initAircraftVariable = func (airport, runway, isGliderPos = 1) {
|
||||
# Set coordinates as glider position or runway threshold
|
||||
g_coord = isGliderPos
|
||||
? geo.aircraft_position()
|
||||
: geo.Coord.new().set_latlon(runway.lat, runway.lon);
|
||||
|
||||
# Set airplane heading as runway heading
|
||||
g_heading = runway.heading;
|
||||
|
||||
# Set airplane altitude as airport elevation
|
||||
g_altitude = airport.elevation * METER_TO_FEET;
|
||||
}
|
||||
|
||||
#
|
||||
# Get distance from glider to runway threshold
|
||||
#
|
||||
# runway - Object of runway from which the glider start
|
||||
# Return the distance in metres, of the glider's displacement from the runway threshold.
|
||||
#
|
||||
var getGliderOffsetRunwayThreshold = func (runway) {
|
||||
var gliderCoord = geo.aircraft_position();
|
||||
var rwyThreshold = geo.Coord.new().set_latlon(runway.lat, runway.lon);
|
||||
|
||||
return rwyThreshold.distance_to(gliderCoord);
|
||||
}
|
||||
|
||||
#
|
||||
# Add new waypoint on ground
|
||||
#
|
||||
# coordOffset - Hash for calculate next coordinates (lat, lon), with following fields:
|
||||
# {
|
||||
# hdgChange - How the aircraft's heading supposed to change? 0 - keep the same heading.
|
||||
# dis - Distance in meters to calculate next waypoint coordinates.
|
||||
# }
|
||||
# performance - Hash with following fields:
|
||||
# {
|
||||
# altChange - How the aircraft's altitude is supposed to change? 0 - keep the same altitude.
|
||||
# ktas - True air speed of AI plane at the waypoint.
|
||||
# }
|
||||
#
|
||||
var addWptGround = func (coordOffset, performance) {
|
||||
wrireWpt(nil, coordOffset, performance, "ground");
|
||||
}
|
||||
|
||||
#
|
||||
# Add new waypoint in air
|
||||
#
|
||||
var addWptAir = func (coordOffset, performance) {
|
||||
wrireWpt(nil, coordOffset, performance, "air");
|
||||
}
|
||||
|
||||
#
|
||||
# Add "WAIT" waypoint
|
||||
#
|
||||
# sec - Number of seconds for wait
|
||||
#
|
||||
var addWptWait = func (sec) {
|
||||
wrireWpt("WAIT", {"hdgChange": nil, "dist": nil}, {"altChange": nil, "ktas": nil}, nil, sec);
|
||||
}
|
||||
|
||||
#
|
||||
# Add "END" waypoint
|
||||
#
|
||||
var addWptEnd = func () {
|
||||
wrireWpt("END", {"hdgChange": nil, "dist": nil}, {"altChange": nil, "ktas": nil});
|
||||
}
|
||||
|
||||
#
|
||||
# Write waipoint to flight plan file
|
||||
#
|
||||
# name - The name of waypoint
|
||||
# coordOffset.hdgChange - How the aircraft's heading supposed to change?
|
||||
# coordOffset.dist - Distance in meters to calculate next waypoint coordinates
|
||||
# performance.altChange - How the aircraft's altitude is supposed to change?
|
||||
# performance.ktas - True air speed of AI plane at the waypoint
|
||||
# groundAir - Allowe value: "ground or "air". The "ground" means that AI plane is on the ground, "air" - in air
|
||||
# sec - Number of seconds for "WAIT" waypoint
|
||||
#
|
||||
var wrireWpt = func (
|
||||
name,
|
||||
coordOffset,
|
||||
performance,
|
||||
groundAir = nil,
|
||||
sec = nil
|
||||
) {
|
||||
var localCoord = nil;
|
||||
if (coordOffset.hdgChange != nil and coordOffset.dist != nil) {
|
||||
g_heading = g_heading + coordOffset.hdgChange;
|
||||
if (g_heading < 0) {
|
||||
g_heading = 360 + g_heading;
|
||||
}
|
||||
|
||||
if (g_heading > 360) {
|
||||
g_heading = g_heading - 360;
|
||||
}
|
||||
|
||||
g_coord.apply_course_distance(g_heading, coordOffset.dist);
|
||||
localCoord = g_coord;
|
||||
}
|
||||
|
||||
var localAlt = nil;
|
||||
if (performance.altChange != nil) {
|
||||
g_altitude = g_altitude + performance.altChange;
|
||||
localAlt = g_altitude;
|
||||
}
|
||||
|
||||
name = name == nil ? g_wptCount : name;
|
||||
var data = getWptString(
|
||||
name,
|
||||
localCoord,
|
||||
localAlt,
|
||||
performance.ktas,
|
||||
groundAir,
|
||||
sec
|
||||
);
|
||||
|
||||
io.write(g_fpFileHandler, data);
|
||||
|
||||
g_wptCount = g_wptCount + 1;
|
||||
}
|
||||
|
||||
#
|
||||
# Get single waypoint data as a string.
|
||||
#
|
||||
# name - Name of waypoint. Special names are: "WAIT", "END".
|
||||
# coord - The Coord object
|
||||
# alt - Altitude AMSL of AI plane
|
||||
# ktas - True air speed of AI plane
|
||||
# groundAir - Allowe value: "ground or "air". The "ground" means that AI plane is on the ground, "air" - in air
|
||||
# sec - Number of seconds for "WAIT" waypoint
|
||||
#
|
||||
var getWptString = func (name, coord = nil, alt = nil, ktas = nil, groundAir = nil, sec = nil) {
|
||||
var str = " <wpt>\n"
|
||||
~ " <name>" ~ name ~ "</name>\n";
|
||||
|
||||
if (coord != nil) {
|
||||
str = str ~ " <lat>" ~ coord.lat() ~ "</lat>\n";
|
||||
str = str ~ " <lon>" ~ coord.lon() ~ "</lon>\n";
|
||||
str = str ~ " <!-- " ~ coord.lat() ~ "," ~ coord.lon() ~ " -->\n";
|
||||
}
|
||||
|
||||
if (alt != nil) {
|
||||
str = str ~ " <alt>" ~ alt ~ "</alt>\n";
|
||||
# str = str ~ " <crossat>" ~ alt ~ "</crossat>\n";
|
||||
}
|
||||
|
||||
if (ktas != nil) {
|
||||
str = str ~ " <ktas>" ~ ktas ~ "</ktas>\n";
|
||||
}
|
||||
|
||||
if (groundAir != nil) {
|
||||
var onGround = groundAir == "ground" ? "true" : "false";
|
||||
str = str ~ " <on-ground>" ~ onGround ~ "</on-ground>\n";
|
||||
}
|
||||
|
||||
if (sec != nil) {
|
||||
str = str ~ " <time-sec>" ~ sec ~ "</time-sec>\n";
|
||||
}
|
||||
|
||||
return str ~ " </wpt>\n";
|
||||
}
|
71
gui/dialogs/about-aerotow.xml
Normal file
71
gui/dialogs/about-aerotow.xml
Normal file
@ -0,0 +1,71 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
#
|
||||
# Aerotow Everywhere - Add-on for FlightGear
|
||||
#
|
||||
# Written and developer by Roman Ludwicki (PlayeRom, SP-ROM)
|
||||
#
|
||||
# Copyright (C) 2022 Roman Ludwicki
|
||||
#
|
||||
# Aerotow Everywhere is an Open Source project and it is licensed
|
||||
# under the GNU Public License v3 (GPLv3)
|
||||
#
|
||||
-->
|
||||
|
||||
<!-- This file requires FlightGear version 2018.2 or newer -->
|
||||
|
||||
<PropertyList>
|
||||
<name>about-aerotow</name>
|
||||
<layout>vbox</layout>
|
||||
<width>400</width>
|
||||
|
||||
<group>
|
||||
<layout>hbox</layout>
|
||||
<empty>
|
||||
<stretch>1</stretch>
|
||||
</empty>
|
||||
<text>
|
||||
<label>About Aerotow Everywhere Add-on</label>
|
||||
</text>
|
||||
<empty>
|
||||
<stretch>1</stretch>
|
||||
</empty>
|
||||
<button>
|
||||
<pref-width>16</pref-width>
|
||||
<pref-height>16</pref-height>
|
||||
<legend></legend>
|
||||
<keynum>27</keynum>
|
||||
<border>2</border>
|
||||
<binding>
|
||||
<command>dialog-close</command>
|
||||
</binding>
|
||||
</button>
|
||||
</group>
|
||||
|
||||
<hrule/>
|
||||
|
||||
<text>
|
||||
<label>Aerotow Everywhere version 1.0.0 - 13th August 2022</label>
|
||||
</text>
|
||||
|
||||
<text>
|
||||
<label>Written by Roman Ludwicki</label>
|
||||
</text>
|
||||
|
||||
<button>
|
||||
<legend>Open the repository website</legend>
|
||||
<binding>
|
||||
<command>nasal</command>
|
||||
<script>fgcommand("open-browser", props.Node.new({"url":"https://github.com/PlayeRom/flightgear-addon-aerotow-everywhere"}));</script>
|
||||
</binding>
|
||||
</button>
|
||||
|
||||
<hrule/>
|
||||
|
||||
<button>
|
||||
<legend>Close</legend>
|
||||
<binding>
|
||||
<command>dialog-close</command>
|
||||
</binding>
|
||||
</button>
|
||||
</PropertyList>
|
67
gui/dialogs/help-aerotow.xml
Normal file
67
gui/dialogs/help-aerotow.xml
Normal file
@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
#
|
||||
# Aerotow Everywhere - Add-on for FlightGear
|
||||
#
|
||||
# Written and developer by Roman Ludwicki (PlayeRom, SP-ROM)
|
||||
#
|
||||
# Copyright (C) 2022 Roman Ludwicki
|
||||
#
|
||||
# Aerotow Everywhere is an Open Source project and it is licensed
|
||||
# under the GNU Public License v3 (GPLv3)
|
||||
#
|
||||
-->
|
||||
|
||||
<!-- This file requires FlightGear version 2018.2 or newer -->
|
||||
|
||||
<PropertyList>
|
||||
<name>help-aerotow</name>
|
||||
<layout>vbox</layout>
|
||||
<width>800</width>
|
||||
|
||||
<group>
|
||||
<layout>hbox</layout>
|
||||
<empty>
|
||||
<stretch>1</stretch>
|
||||
</empty>
|
||||
<text>
|
||||
<label>Aerotow Everywhere Help</label>
|
||||
</text>
|
||||
<empty>
|
||||
<stretch>1</stretch>
|
||||
</empty>
|
||||
<button>
|
||||
<pref-width>16</pref-width>
|
||||
<pref-height>16</pref-height>
|
||||
<legend></legend>
|
||||
<keynum>27</keynum>
|
||||
<border>2</border>
|
||||
<binding>
|
||||
<command>dialog-close</command>
|
||||
</binding>
|
||||
</button>
|
||||
</group>
|
||||
|
||||
<textbox>
|
||||
<name>help</name>
|
||||
<halign>fill</halign>
|
||||
<valign>fill</valign>
|
||||
<stretch>true</stretch>
|
||||
<pref-width>750</pref-width>
|
||||
<pref-height>600</pref-height>
|
||||
<padding>6</padding>
|
||||
<slider>20</slider>
|
||||
<editable>false</editable>
|
||||
<wrap>true</wrap>
|
||||
<property>/addons/by-id/org.flightgear.addons.Aerotow/addon-devel/help-text</property>
|
||||
</textbox>
|
||||
|
||||
<hrule/>
|
||||
|
||||
<button>
|
||||
<legend>OK</legend>
|
||||
<binding>
|
||||
<command>dialog-close</command>
|
||||
</binding>
|
||||
</button>
|
||||
</PropertyList>
|
54
messages.nas
Normal file
54
messages.nas
Normal file
@ -0,0 +1,54 @@
|
||||
#
|
||||
# Aerotow Everywhere - Add-on for FlightGear
|
||||
#
|
||||
# Written and developer by Roman Ludwicki (PlayeRom, SP-ROM)
|
||||
#
|
||||
# Copyright (C) 2022 Roman Ludwicki
|
||||
#
|
||||
# Aerotow Everywhere is an Open Source project and it is licensed
|
||||
# under the GNU Public License v3 (GPLv3)
|
||||
#
|
||||
|
||||
#
|
||||
# Display given message as OK.
|
||||
#
|
||||
# message - The text message to display on the screen and read by speech synthesizer.
|
||||
#
|
||||
var displayOk = func (message) {
|
||||
display(message, "ok");
|
||||
}
|
||||
|
||||
#
|
||||
# Display given message as an error.
|
||||
#
|
||||
# message - The text message to display on the screen and read by speech synthesizer.
|
||||
#
|
||||
var displayError = func (message) {
|
||||
display(message, "error");
|
||||
}
|
||||
|
||||
#
|
||||
# Display given message.
|
||||
#
|
||||
# message - The text message to display on the screen and read by speech synthesizer.
|
||||
# type - The type of message. It can take values as "ok" or "error".
|
||||
#
|
||||
var display = func (message, type) {
|
||||
# Read the message by speech synthesizer
|
||||
props.globals.getNode("/sim/sound/voices/ai-plane").setValue(message);
|
||||
|
||||
# Display message on the screan
|
||||
var durationInSec = int(size(message) / 12) + 3;
|
||||
var window = screen.window.new(nil, -40, 10, durationInSec);
|
||||
window.bg = [0.0, 0.0, 0.0, 0.40];
|
||||
|
||||
if (type == "error") {
|
||||
window.fg = [1.0, 0.0, 0.0, 1];
|
||||
}
|
||||
else {
|
||||
window.fg = [0.0, 1.0, 0.0, 1];
|
||||
}
|
||||
|
||||
window.align = "center";
|
||||
window.write(message);
|
||||
}
|
Loading…
Reference in New Issue
Block a user