diff --git a/CHANGELOG.md b/CHANGELOG.md index b89006654..77daea31e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,76 @@ kOS Mod Changelog ================= +# v1.1.9.0 Breaking Bounds + +This update is a mix of new features, mostly + +### BREAKING CHANGES + +### NEW FEATURES + +- Bounding box information for parts and for the vessel as + a whole is now exposed for scripts to read. + [pull request 1](https://github.com/KSP-KOS/KOS/pull/2563). + [pull request 2](https://github.com/KSP-KOS/KOS/pull/2564). +- The above bounding box feature also came with some new suffixes + for Vecdraw so you can now draw plain lines (suppress the + arrowhead, suppress the opacity fade) with them. +- Lexicons can now use the suffix syntax. i.e. where you + say ``mylex["key1"]`` you can now say ``mylex:key1``, + provided the key is something that works as a valid identifier + string (no spaces, etc). + [pull request](https://github.com/KSP-KOS/KOS/pull/2553). +- Can now set the default terminal Width and Height for all + newly spawned terminals. + [pull request 1](https://github.com/KSP-KOS/KOS/pull/2573). +- A ternary conditional operator exists in kerboscript now, + using the syntax ``CHOOSE expr1 IF bool_expr ELSE expr2``. + If *bool_expr* is true, it returns expr1. If it's false, + it returns expr2. + [pull request](https://github.com/KSP-KOS/KOS/pull/2549). +- Added support to read more atmospheric values from KSP. + [pull request](https://github.com/KSP-KOS/KOS/pull/2557). + +### BUG FIXES + +- TimeSpan now peeks at the KSP game to learn its notion of + how long a day is, and how long a year is, rather than hardcoding + the values. + [pull request](https://github.com/KSP-KOS/KOS/pull/2582). +- Fix cooked control triggers not working during a WHEN/ON trigger. + [pull request](https://github.com/KSP-KOS/KOS/pull/2534). +- Fix mangled state if kOS is out of electricity when scenes switch + or the game is saved. + [pull request](https://github.com/KSP-KOS/KOS/pull/2521). +- Obsolete list command documentation removed. + [pull request](https://github.com/KSP-KOS/KOS/pull/2520). +- Allow part modules'd fields to work even when no GUI name is defined. + It seems that the main game allows the GUI name to be left out and if + so it inherits from the base name under tne hood. Now kOS follows + this behaviour. + [pull request](https://github.com/KSP-KOS/KOS/pull/2519). +- Prevent using UNSET on built-in variable names like SHIP, ALTITUDE, + and so on. + [pull request](https://github.com/KSP-KOS/KOS/pull/2510). +- RP-1 used a different technique to lock out controls due to + insufficient avionics that kOS didn't know about. kOS bypassed + this lockout and still controlled the vessel anyway. This is no + longer the case. + [pull request](https://github.com/KSP-KOS/KOS/pull/2546). +- PartModule:SETFIELD now works properly with the new type of slider + widget that robotic parts use in KSP 1.7.x. KSP introduced a new + type of slider widget that presents false information when kOS tried + to obey its min, max, and detent values, those being only dummy + placeholders for these types of sliders, not actually populated with + the real values. For these sliders, the real limit values come from + another field, requiring a more indirect method call to get the information. + [pull request](https://github.com/KSP-KOS/KOS/pull/2554). +- GUI windows no longer use the KSP control lock system to emulate + keyboard focus, instead relying on the built-in Unity IMGUI + focus rules for widgets, thus they won't 'steal focus' as much. + [pull request](https://github.com/KSP-KOS/KOS/pull/2577). + # v1.1.8.0 Engines and KSP 1.7 compatibility Mostly this was motivated by a need to get an officially diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 48f9d9371..28a391524 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -43,18 +43,27 @@ Pull Requests ####Nobody merges their own PR +NOTE THIS RULE IS SUSPENDED. + +THIS RULE IS SUSPENDED BECAUSE THE DEV TEAM SHRUNK TO JUST ONE +PERSON, MAKING IT IMPOSSIBLE TO GET ANY PR MERGED IF THIS RULE +WAS STILL BEING FOLLOWED. (If the dev team grows again to more +people, this rule may be re-instated, as it is very good practice, +WHEN there's actually more than one person on the team who has the +time.) + (Rules for priviledged members of the team who have permission to write directly to the main repository.) -1. As a general policy, even experienced developers on the team should not +1. (SUSPENDED - SEE ABOVE) As a general policy, even experienced developers on the team should not merge their own pull requests into the upstream `develop`. Instead they should get another developer to merge it for them. -2. As such, even if you have permission to do so, never directly push a +2. (SUSPENDED - SEE ABOVE) As such, even if you have permission to do so, never directly push a change to `upstream develop` except in cases where you are doing so as part of the process of merging somebody *else's* pull request other than your own, or during some of the final steps of the release checklist that require it. -3. When merging somebody else's pull request, do not "rubber stamp" it. Actually +3. (SUSPENDED - SEE ABOVE) When merging somebody else's pull request, do not "rubber stamp" it. Actually try to read and understand what it does and how, and raise questions with the author using the github "line note" system. @@ -146,12 +155,25 @@ Setting Up Your Environment pull request to KSP-KOS/KOS `develop` for review to be included. ####Setting Up The Solution Dependencies + 1. Copy the folder `$KOS/Resources/GameData/kOS` to `$KSP/GameData/` -2. Copy `Assembly-CSharp.dll`, `Assembly-CSharp-firstpass.dll`, - `UnityEngine.dll` and `UnityEngine.UI.dll` from `$KSP/KSP_Data/Managed` - into `$KOS/Resources`. If you do not have a copy of KSP locally, you may - download dummy assemblies at https://github.com/KSP-KOS/KSP_LIB +2. Get the Unity assemblies into your project. There are two options: + 1. Copy these DLLs from `$KSP/KSP_Data/Managed `into `$KOS/Resources`: + * `Assembly-CSharp` + * `Assembly-CSharp-firstpass` + * `UnityEngine` + * `UnityEngine.AnimationModule` + * `UnityEngine.AudioModule` + * `UnityEngine.CoreModule` + * `UnityEngine.ImageConversionModule` + * `UnityEngine.IMGUIModule` + * `UnityEngine.PhysicsModule` + * `UnityEngine.TextRenderingModule` + * `UnityEngine.UI` + * `UnityEngine.UnityWebRequestWWWModule` + 2. If you do not have a copy of KSP locally, you may + download dummy assemblies at https://github.com/KSP-KOS/KSP_LIB 3. If you want building the solution to update the dlls in your KSP directory, create a symbolic link called `KSPdirlink` from the root diff --git a/Resources/GameData/kOS/kOS.version b/Resources/GameData/kOS/kOS.version index 9ded6f03d..49a30a4d3 100644 --- a/Resources/GameData/kOS/kOS.version +++ b/Resources/GameData/kOS/kOS.version @@ -11,13 +11,13 @@ "VERSION": { "MAJOR": 1, "MINOR": 1, - "PATCH": 8, + "PATCH": 9, "BUILD": 0 }, "KSP_VERSION": { "MAJOR": 1, "MINOR": 7, - "PATCH": 0 + "PATCH": 3 }, "KSP_VERSION_MIN": { "MAJOR": 1, @@ -27,6 +27,6 @@ "KSP_VERSION_MAX": { "MAJOR": 1, "MINOR": 7, - "PATCH": 0 + "PATCH": 99 } } diff --git a/doc/README.md b/doc/README.md index 61627fe45..938e17074 100644 --- a/doc/README.md +++ b/doc/README.md @@ -3,7 +3,7 @@ KOS_DOC The documents are generated using Sphinx restructured text, with the ReadTheDocs theme. -#Getting started on Windows +# Getting started on Windows (For this example, the KOS repository is assumed to be located at `C:\KOS`, you should adjust your path based on your actual repository location) @@ -51,14 +51,14 @@ you should adjust your path based on your actual repository location) At which point you can point your browser to `http://localhost:8000` -#Getting started on Linux +# Getting started on Linux 1. As with Windows above, install Python 2.7. You may use your distribution's package manager system, or download from: https://www.python.org/downloads/ 2. All other instructions are the same as above for windows, replacing the `\` path character with `/` and adapting paths to reference your Linux file system. -#Publishing +# Publishing This section pertains only to what has to be done when a new release of the documentation is being made public (usually to correspond to a new @@ -134,3 +134,23 @@ performed on every single documentation edit and every merged PR. ``` C:\KOS-gh-pages>git push KOS_DOC gh-pages ``` + +# Generating Dash Docset + +[Dash](https://kapeli.com/dash) is an offline API Documentation Browser and +Code Snippet Manager for macOS. + +1. Install the [doc2dash][doc-2-dash] python package using `pipx` as + recommended in the linked instructions. +2. Follow the instructions in "Getting started on Windows" adapting for macOS + as necessary. (Basically the Linux instructions, but no need to install + python). +3. After compiling the RTD docs, use doc2dash to create a docset. From the + `doc/` folder run: + ``` + pipx run doc2dash -n kOS -A gh-pages --icon=source/_images/kos_logo_konly.png + ``` +4. If this completes successfully and you have Dash installed, the docset will + automatically open and be added to your docset library. + +[doc-2-dash]: https://doc2dash.readthedocs.io/en/stable/installation.html diff --git a/doc/source/_images/kos_logo_konly.png b/doc/source/_images/kos_logo_konly.png new file mode 100644 index 000000000..1c97904c1 Binary files /dev/null and b/doc/source/_images/kos_logo_konly.png differ diff --git a/doc/source/_images/structures/vessels/bounding_part.png b/doc/source/_images/structures/vessels/bounding_part.png new file mode 100644 index 000000000..eaa5e0672 Binary files /dev/null and b/doc/source/_images/structures/vessels/bounding_part.png differ diff --git a/doc/source/_images/structures/vessels/bounding_vessel.png b/doc/source/_images/structures/vessels/bounding_vessel.png new file mode 100644 index 000000000..dd7d00b61 Binary files /dev/null and b/doc/source/_images/structures/vessels/bounding_vessel.png differ diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 76da8195e..6df021bcb 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -24,6 +24,76 @@ release. **** +Changes in 1.1.9.0 +------------------ + +BOUNDING BOX +:::::::::::: + +Added the new :struct:`BOUNDS` structure for bounding box +information, and made an :ref:`example using it ` +on the tutorials page. + +TERNARY OPERATOR "CHOOSE" +::::::::::::::::::::::::: + +A new expression ternary operator exists in kerboscript, called +:ref:`CHOOSE `. (Similar to C's "?" operator, but with +different syntax.) + +New suffixes for Vecdraw +:::::::::::::::::::::::: + +New suffixes giving you more control over the appearance of +vecdraws: :attr:`Vecdraw:POINTY` :attr:`Vecdraw:WIPING` + +Lexicon Suffixes +:::::::::::::::: + +:ref:`Describe using suffixes with lexicons. ` + +Terminal default size +::::::::::::::::::::: + +Two new config settings for a default terminal size for +new terminals: + +:struct:`Config:DEFAULTWIDTH`, :struct:`Config:DEFAULTHEIGHT` + +Additional Atmospheric information +::::::::::::::::::::::::::::::::::: + +Added some more information to the :struct:`atmosphere` structure, +(mostly for people trying to perform drag calculations: +MOLARMASS, ADIABATICINDEX, ALTITUDETEMPERATURE). + +Also added the ability to read some more of the values the +game uses for :ref:`mathematical constants `, to +work with this information: Avogadro, Boltzmann, and IdealGas. + +UNSET documentation +::::::::::::::::::: + +Explicitly mention the :ref:`unset command `, which has existed +for a long time but apparently wasn't in the documentation. + +LIST command +:::::::::::: + +Removed obsolete documentation about a no-longer-existing "FROM" +variant of the LIST command that went like this: +LIST *things* FROM *vessel* IN *variable*. + +DROPPRIORITY() +:::::::::::::: + +Described the new :func:`DROPPRIORITY()` built-in function that you +can use when you want to write a long-lasting trigger body without +it preventing other triggers from interrupting like it normally would. + + + + Changes in 1.1.8.0 ------------------ diff --git a/doc/source/commands/flight/cooked.rst b/doc/source/commands/flight/cooked.rst index 0b730699a..f830535b6 100644 --- a/doc/source/commands/flight/cooked.rst +++ b/doc/source/commands/flight/cooked.rst @@ -184,17 +184,18 @@ Like all ``LOCK`` expressions, the steering and throttle continually update on t LOCK WHEELSTEERING. See the note in the next section below. -Don't 'WAIT' during cooked control calculation ----------------------------------------------- +Don't 'WAIT' or run slow script code during cooked control calculation +---------------------------------------------------------------------- Be aware that because LOCK THROTTLE, LOCK STEERING, LOCK -WHEELTHROTTLE, and LOCK WHEELSTEERING are actually -:ref:`triggers ` that cause your expression -to be calculated every single physics update tick behind -the scenes, you should not execute a ``WAIT`` command -in the code that performs the evaluation of the value -used in them, as that will effectively cheat the entire -script out of the full execution speed it deserves. +WHEELTHROTTLE, and LOCK WHEELSTEERING are actually the +highest priority types of :ref:`triggers ` that +exist in kOS, they cause your expression to be calculated +every single physics update tick behind the scenes. So you +should not execute a ``WAIT`` command in the code that +performs the evaluation of the value used in them, as that +will effectively cheat the entire script out of the full +execution speed it deserves. For example, if you attempt this:: @@ -216,6 +217,13 @@ there, not resuming until the next update, effectively meaning it doesn't get around to running any of your main-line code until the next tick.) +Again, note that the cooked steering LOCKS mentioned here are +the *highest* priority triggers there are in kOS. That means they +can even interrupt other triggers like WHEN/THEN or GUI callbacks. +Do not make them call complex functions that take a lot of instructions +to return a value, or else you might find that there's not enough +instructions per update left to run the rest of your program effectively. + Normally when you use a LOCK command, the expression is only evaluated when it needs to be by some other part of the script that is trying to read the value. But with these special cooked control locks, diff --git a/doc/source/commands/flight/systems.rst b/doc/source/commands/flight/systems.rst index 8c2d92d21..4c5552857 100644 --- a/doc/source/commands/flight/systems.rst +++ b/doc/source/commands/flight/systems.rst @@ -59,7 +59,7 @@ RCS and SAS SET SASMODE TO value. - It is the equivalent to clicking on the buttons next to the nav ball while manually piloting the craft, and will respect the current mode of the nav ball (orbital, surface, or target velocity - use NAVMODE to read or set it). Valid strings for ``value`` are ``"PROGRADE"``, ``"RETROGRADE"``, ``"NORMAL"``, ``"ANTINORMAL"``, ``"RADIALOUT"``, ``"RADIALIN"``, ``"TARGET"``, ``"ANTITARGET"``, ``"MANEUVER"``, ``"STABILITYASSIST"``, and ``"STABILITY"``. A null or empty string will default to stability assist mode, however any other invalid string will throw an exception. This feature will respect career mode limitations, and will throw an exception if the current vessel is not able to use the mode passed to the command. An exception is also thrown if ``"TARGET"`` or ``"ANTITARGET"`` are used, but no target is selected. + It is the equivalent to clicking on the buttons next to the nav ball while manually piloting the craft, and will respect the current mode of the nav ball (orbital, surface, or target velocity - use NAVMODE to read or set it). Valid strings for ``value`` are ``"PROGRADE"``, ``"RETROGRADE"``, ``"NORMAL"``, ``"ANTINORMAL"``, ``"RADIALOUT"``, ``"RADIALIN"``, ``"TARGET"``, ``"ANTITARGET"``, ``"MANEUVER"``, ``"STABILITYASSIST"``, and ``"STABILITY"``. A null or empty string will default to stability assist mode, however any other invalid string will throw an exception. This feature will respect career mode limitations, and will throw an exception if the current vessel is not able to use the mode passed to the command. An exception is also thrown if ``"TARGET"`` or ``"ANTITARGET"`` are used when no target is set. .. note:: SAS mode is reset to stability assist when toggling SAS on, however it doesn't happen immediately. @@ -388,4 +388,13 @@ TARGET For more information see :ref:`bindings`. + NOTE, the way to de-select the target is to set it to an empty + string like this:: + + SET TARGET TO "". // de-selects the target, setting it to nothing. + + (Trying to use :ref:`UNSET TARGET.` will have no effect because + ``UNSET`` means "get rid of the variable itself" which you're not + allowed to do with built-in bound variables like ``TARGET``.) + Note that the above options also can refer to a different vessel besides the current ship, for example, ``TARGET:THROTTLE`` to read the target's throttle. But not all "set" or "lock" options will work with a different vessel other than the current one, because there's no authority to control a craft the current program is not attached to. diff --git a/doc/source/commands/list.rst b/doc/source/commands/list.rst index c48ed0607..38e26fb10 100644 --- a/doc/source/commands/list.rst +++ b/doc/source/commands/list.rst @@ -10,7 +10,7 @@ A :struct:`List` is a type of :ref:`Structure ` that stores ``FOR`` Loop ------------ -Lists need to be iterated over sometimes, to help with this we have the :ref:`FOR loop, explained on the flow control page `. The ``LIST`` Command comes in 4 forms: +Lists need to be iterated over sometimes, to help with this we have the :ref:`FOR loop, explained on the flow control page `. The ``LIST`` Command comes in 3 forms: 1. ``LIST.`` When no parameters are given, the LIST command is exactly equivalent to the command:: @@ -23,9 +23,6 @@ Lists need to be iterated over sometimes, to help with this we have the :ref:`FO 3. ``LIST ListKeyword IN YourVariable.`` This variant takes the items that would otherwise have been printed to the terminal screen, and instead makes a :struct:`List` of them in ``YourVariable``, that you can then iterate over with a :ref:`FOR loop ` if you like. -4. ``LIST ListKeyword FROM SomeVessel IN YourVariable.`` - This variant is just like variant (3), except that it gives a list of the items that exist on some other vessel that might not necessarily be the current :ref:`CPU_vessel `. - Available Listable Keywords --------------------------- @@ -118,8 +115,8 @@ Here are some more examples:: PRINT "The mass of the whole solar system is " + totMass. // Adds variable foo that contains a list of - // resources for my currently target vessel - LIST RESOURCES FROM TARGET IN foo. + // resources for my current vessel + LIST RESOURCES IN foo. FOR res IN foo { PRINT res:NAME. // Will print the name of every // resource in the vessel diff --git a/doc/source/conf.py b/doc/source/conf.py index 6307b8bbe..5cb31ed44 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -59,9 +59,9 @@ # built documents. # # The short X.Y version. -version = '1.1.8.0' +version = '1.1.9.0' # The full version, including alpha/beta/rc tags. -release = '1.1.8.0' +release = '1.1.9.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/doc/source/general/cpu_hardware.rst b/doc/source/general/cpu_hardware.rst index 8d5691781..794ead139 100644 --- a/doc/source/general/cpu_hardware.rst +++ b/doc/source/general/cpu_hardware.rst @@ -226,6 +226,20 @@ short and fast to execute. If it consists of multiple clauses, try to take advantage of* :ref:`short circuit boolean ` *logic by putting the fastest part of the check first.* +Triggers for GUI callbacks +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Another type of trigger is the callback delegates that you can +write for the :ref:`GUI system ` when using the +:ref:`Callback technique `. (For example, +using :attr:`Button:ONCLICK`, :attr:`Slider:ONCHANGE`, and so on.) + +When you give a GUI a callback hook to call, the CPU will implement +that as a trigger as well. When you click the button or move the +slider, etc, then kOS will interrupt your program at the next available +opportunity (usually the start of the next IPU's worth of instructions), +to call your callback delegate. + .. _wait_in_trigger: Wait in a Trigger @@ -236,6 +250,15 @@ the script to use it, it's probably not a good design choice to use ``WAIT`` inside a trigger. Triggers should be designed to execute all the way through to the end in one fast pass, if possible. +Exception: If you are careful, there is a built-in function you +can call that will have your trigger willingly relinquish its priority +increase, reducing it back down to whatever the priority was before +it rudely interrupted things. Doing that can allow other triggers of +equal priority to itself to interrupt it again. To see how this works, +look at :func:`DROPPRIORITY()`, explained below on this page. In general, +however, it's a better idea not to use this unless you fully understand +how the prioriy system here works. + Do Not Loop a Long Time in a Trigger Body! ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -256,6 +279,15 @@ executed, and other triggers of equal or lesser priority aren't being executed. A trigger that performs a long-running loop will starve the rest of the code in your kerboscript program from being allowed to run. +Exception: If you are careful, there is a built-in function you +can call that will have your trigger willingly relinquish its priority +increase, reducing it back down to whatever the priority was before +it rudely interrupted things. Doing that can allow other triggers of +equal priority to itself to interrupt it again. To see how this works, +look at :func:`DROPPRIORITY()`, explained below on this page. In general, +however, it's a better idea not to use this unless you fully understand +how the prioriy system here works. + But I Want a Loop!! ~~~~~~~~~~~~~~~~~~~ @@ -282,8 +314,9 @@ is allowed to interrupt the program flow depending on what the program is doing right now. This is accomplished by having a few priority levels, shown in this list: -* Priority 20: :ref:`Recurring Interrupts ` -* Priority 10: :ref:`Callback-Once Interrupts ` +* Priority 30: :ref:`Cooked control Interrupts ` (i.e. LOCK STEERING) +* Priority 20: :ref:`Recurring Interrupts ` (i.e. WHEN or ON) +* Priority 10: :ref:`Callback-Once Interrupts ` (i.e. GUI callbacks) * Priority 0: Normal (non-interrupting) code. **A Trigger will only interrupt something of lower priority than itself**. @@ -302,7 +335,7 @@ to happen again and again with speed, while the callback-once interrupts are probably not as time-sensitive since they respond to one-shot events like user clicks. -**A trigger cannot interrupt *itself* if it's still running**. +**most triggers cannot interrupt *themselves* if they're still running**. When you have recurring triggers that keep re-running themselves again and again, the way they work is that they wait till the previous @@ -321,6 +354,156 @@ is too dependant on the priorities being exactly this way. (This is why these numbers aren't even exposed to the script at the moment, to avoid that design pattern.) + +.. _drop_priority: + +Deliberately reducing your priority in long running triggers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Normally if you did something like this:: + + local done is false. + + set Gwin to GUI(200). + set b1 to Gwin:addbutton("beep"). + set b1:onclick to { getvoice(0):play(note(300,0.2)). }. + set b2 to GWin:addbutton("count"). + set b2:onclick to count@. + set b3 to Gwin:addbutton("quit"). + set b3:onclick to { set done to true. }. + + GWin:show(). + wait until done. + GWin:Dispose(). + + function count { + local i is 5. + until i = 0 { + print "Counting.. " + i. + set i to i - 1. + wait 1. + } + } + +It would mean that while you press the "count" button, and it prints the +countdown from 5 to 1, the other buttons, including "beep" and "quit" +would have no effect until the countdown is done. Because ``count()`` +is the callback for a GUI button, it runs at a higher than normal priority, +which means it won't let itself get interrupted by other GUI callbacks. +Instead those other GUI callbacks will be delayed until count() is done. + +If you wish, you can cause your trigger, or callback, to deliberately +relinquish its hold on other interrupts, allowing them to interrupt it +despite the fact that it is itself in the middle of an interrupt. +You do this by deliberately reducing your current priority level +back down a step to whatever it was prior to being incresed by the +interrrupt, which is what this special built-in function does: + +.. function:: DROPPRIORITY() + + After this built-in function is executed by a trigger's body, + the current interrupt priority is dropped back down to whatever the + priority of the code you interrupted was. This is your trigger's + way of saying "I don't actually want to block interrupts anymore. + Please let me be interrupted just as much as whatever *I* + interrupted was allowed to be interrupted." + + SO, for example, if Priority 0 code (normal code) got interrupted + by priority 10 code (GUI callback code), and the GUI callback + code executed ``DROPPRIORITY``, then it would now be running at + priority 0 instead of 10, because priority 0 is what got interrupted, + and thus allow other GUI code to interrupt it again. + + On the other hand, if GUI callback code (priority 10) got + interrupted by WHEN-THEN code (priority 20), and the WHEN-THEN + code had called DROPPRIORITY(), then the priority level of + that pass through the WHEN-THEN would only be dropped down to + 10, NOT all the way to 0, because it was interrupting priority 10 + code. + + The reason it works this way (instead of just dropping it all the + way down to normal (0) priority directly) is that, effectively, + it means a trigger only has the authority to undo its own + priority increase that it caused itself. It can't force the + priority down to something less than the code that got interrupted + had to begin with. Had it been allowed to do that, it could have + been a back-door to circumventing the priority of the thing + that it interrupted. + + Be aware that once you ``DROPPRIORITY()``, you also are making it + so that the SAME trigger you are currently inside of could fire off + again too. It may be a good idea to protect yourself against that, + if it is not desired, by setting a flag variable to record the fact + that you are inside the trigger at the time and should not re-run it, + and then test this flag variable at the top of your trigger code, + skipping the body if it's set. + +So in the above GUI example, if you added ``DROPPRIORITY`` as shown +in the edited version of the example, below, then the other buttons +like the "beep" button, would work while the count() is happening:: + + local done is false. + + set Gwin to GUI(200). + set b1 to Gwin:addbutton("beep"). + set b1:onclick to { getvoice(0):play(note(300,0.2)). }. + set b2 to GWin:addbutton("count"). + set b2:onclick to count@. + set b3 to Gwin:addbutton("quit"). + set b3:onclick to { set done to true. }. + + GWin:show(). + wait until done. + GWin:Dispose(). + + function count { + + DROPPRIORITY(). // <--- NEW LINE ADDED HERE + + local i is 5. + until i = 0 { + print "Counting.. " + i. + set i to i - 1. + wait 1. + } + } + +Once you call ``DROPPRIORITY()``, then from then on, you are effectively no +longer a trigger, as far as the interruption system is concerned. + +BE CAREFUL - if you do this then it is possible for the same trigger or +callback to interrupt *itself* again. In the above example where +DROPPRIORITY() was added, you could press the "count" button twice in +quick succession and one press would interrupt the other. It's up to you, +if you use ``DROPPRIORITY()`` to deal with this problem and stop it from +happening if it's a bad thing for your program. You can do this by +setting a flag that checks if your trigger is already running and if so, +skips it, like so:: + + local count_is_running is false. + function count { + + if not(count_is_running) { + set count_is_running to true. + DROPPRIORITY(). + + local i is 5. + until i = 0 { + print "Counting.. " + i. + set i to i - 1. + wait 1. + } + set count_is_running to false. + } + } + +Again, using ``DROPPRIORITY()`` is an advanced topic that should be avoided +until after you understand what you've read here. Even then, it's usually +simpler and better to just avoid using it and instead design your script in +such a way that it's unnecessary to use it. (It's only necessary to use it +if you have interrupt triggers that run a long time instead of finishing +quickly like they should.) + Wait!!! ------- diff --git a/doc/source/language/flow.rst b/doc/source/language/flow.rst index c6a0ec1eb..a1bc6a07c 100644 --- a/doc/source/language/flow.rst +++ b/doc/source/language/flow.rst @@ -61,6 +61,32 @@ Checks if the expression supplied returns true. If it does, ``IF`` executes the // syntax error - ELSE without IF. IF X > 10 { PRINT "Large". }. ELSE { PRINT "Small". }. +.. index:: CHOOSE +.. _choose: + +CHOOSE (Ternary operator) +------------------------- + +An expression that evalualtes to one of two choices depending on a +conditional check: + + CHOOSE expression1 IF condition ELSE expression2 + +Note this is NOT a statement. This is an expression that can be embedded +inside other statements, like so: + + SET X TO CHOOSE expression1 IF condition ELSE expression2. + PRINT CHOOSE "High" IF altitude > 20000 ELSE "Low". + +The reason to use the ``CHOOSE`` operator instead of an +IF/ELSE statement is that IF/ELSE won't return a value, while +this does, and thus this can be embedded inside other expressions. + +(This is similar to the ``?`` operator in languages like "C" and its +derivatives, except it puts the "true" choice first, then the +conditional check, then the "false" choice.) + + .. index:: LOCK .. _lock: diff --git a/doc/source/language/syntax.rst b/doc/source/language/syntax.rst index 6723ce585..99aaef583 100644 --- a/doc/source/language/syntax.rst +++ b/doc/source/language/syntax.rst @@ -95,8 +95,10 @@ The rest may be letters, digits or underscores. the kOS developers cannot test every language and verify if this is correct or not. -**Suffixes** - Some variable types are structures that contain sub-portions. The separator between the main variable and the item inside it is a colon character (``:``). When this symbol is used, the part on the right-hand side of the colon is called the "suffix":: +Suffixes +-------- + +Some variable types are structures that contain sub-portions. The separator between the main variable and the item inside it is a colon character (``:``). When this symbol is used, the part on the right-hand side of the colon is called the "suffix":: list parts in mylist. print mylist:length. // length is a suffix of mylist @@ -219,6 +221,22 @@ Some suffixes are actually functions you can call. When that is the case, these x:remove(0). x:clear(). +Suffixes as Lexicon keys +------------------------ + +The special type called a :struct:`Lexicon` can be used with this suffix syntax as +an alternate way to get the value for a key, as in the example below:: + + // Given this setup... + set MyLex to Lexicon(). + MyLex:ADD( "key1", "value1"). + // ...these two lines have the same effect: + print MyLex["key1"]. // key used in the usual way as an "index". + print MyLex:key1. // key used in an alternate way as a "suffix". + +There are some limits to using this syntax, as described in more detail +:ref:`in the documentation for the Lexion type `. + .. _syntax functions: User Functions diff --git a/doc/source/language/variables.rst b/doc/source/language/variables.rst index 83e700e01..7193add14 100644 --- a/doc/source/language/variables.rst +++ b/doc/source/language/variables.rst @@ -268,6 +268,32 @@ This follows the :ref:`scoping rules explained below `. If the variable can be found in the current local scope, or any scope higher up, then it won't be created and instead the existing one will be used. +.. _unset: + +``UNSET`` +--------- + +Removes a user-defined variable, if one exists with the given name. + + UNSET X. + UNSET myvariable. + +If there are two variables with the same name, one that is "more local" +and one that is "more global", it will choose the "more local" one to +be removed, according to the usual +:ref:`scoping rules explained below `. + +After this is executed, the variable becomes undefined. + +``UNSET`` cannot be used on a kOS built-in bound variable name, for +example "TARGET", "GEAR", "THROTTLE", "STEERING", etc. It only works +variables that your script created. + +If ``UNSET`` does not find a variable to remove, or it fails to remove +the variable because it is a built-in name as explained above, then +it will NOT generate an error. It will simply quietly move on to the +next statement, doing nothing. + .. _defined: ``DEFINED`` diff --git a/doc/source/math/basic.rst b/doc/source/math/basic.rst index fdb81c470..59e079528 100644 --- a/doc/source/math/basic.rst +++ b/doc/source/math/basic.rst @@ -39,6 +39,12 @@ constants about the universe that you may find handy in your math operations. P - Conversion constant: Degrees to Radians. * - :global:`RadToDeg` - Conversion constant: Radians to Degrees. + * - :global:`Avogadro` + - Avogadro's Constant + * - :global:`Boltzmann` + - Boltzmann's Constant + * - :global:`IdealGas` + - The Ideal Gas Constant .. global:: Constant:G @@ -56,7 +62,7 @@ constants about the universe that you may find handy in your math operations. P within that game universe, and instead varies from one sphere of influence to the next. Such a universe would be breaking some laws of physics by a lot, but it is technically possible - in the game's data model. Due to this strange misfeature in + in the game's data model. Due to this strange feature in the game's data model, it is probably safer to always have your scripts use the body's Mu in your formulas instead of explicitly doing mass*G to derive it. @@ -85,19 +91,19 @@ constants about the universe that you may find handy in your math operations. P contains an inherent conversion from mass to weight that basically means, "what would this mass of fuel have weighed at g0?". Some kind of official standard - value of g0 is needed to use ISP to predict truly - accurately how much fuel will be burned in a scenario. - - In pretty much any other calculation other than using - ISP in the Rocketry Equation, you should probably - not use g0 and instead calculate your local gravity - more precisely based on your actual radius to the body - center. Not only because this is more accurate, but - because the g0 you see here is NOT the g0 you would - actually have on Kerbin's sea level. It's the g0 on - Earth, which is what the game's ISP numbers are using. - Kerbin's sea level g0 is ever so slightly different - from Earth's g0 (but not by much.) + value of g0 is needed to use ISP properly to predict + how much fuel will be burned in a scenario. + + In pretty much any other calculation you do in your kOS + scripts, other than when using ISP in the Rocketry Equation, + you should probably not use g0 and instead calculate your + local gravity more precisely based on your actual radius to + the body center. Not only because this is more accurate, but + because the g0 you see here is NOT the g0 you would actually + have on Kerbin's sea level. It's the g0 on Earth, which is + what the game's ISP numbers are using. Kerbin's sea level + g0 is ever so slightly different from Earth's g0 (but not + by much.) :: @@ -199,6 +205,30 @@ constants about the universe that you may find handy in your math operations. P PRINT "A radian is:". PRINT 1 * constant:RadToDeg + " degrees". +.. global:: Constant:Avogadro + + Avogadro's Constant. + + This value can be used in calculating atmospheric properties for drag purposes, + which can be a rather advanced topic. + `(Avogadro's constant Wikipedia Page) `_. + +.. global:: Constant:Boltzmann + + Boltzmann Constant. + + This value can be used in calculating atmospheric properties for drag purposes, + which can be a rather advanced topic. + `(Boltzmann constant Wikipedia Page) `_. + +.. global:: Constant:IdealGas + + Ideal Gas Constant. + + This value can be used in calculating atmospheric properties for drag purposes, + which can be a rather advanced topic. + `(Ideal Gas Constant Wikipedia Page) `_. + .. _math functions: .. index:: Mathematical Functions @@ -214,9 +244,9 @@ Mathematical Functions :func:`LN(a)` natural log :func:`LOG10(a)` log base 10 :func:`MOD(a,b)` modulus - :func:`MIN(a,b)` minimum - :func:`MAX(a,b)` maximum - :func:`RANDOM()` random number + :func:`MIN(a,b)` return a or b, whichever is lesser. + :func:`MAX(a,b)` return a or b, whichever is greater. + :func:`RANDOM()` random fractional number between 0 and 1. :func:`ROUND(a)` round to whole number :func:`ROUND(a,b)` round to nearest place :func:`SQRT(a)` square root @@ -276,9 +306,18 @@ Mathematical Functions .. function:: RANDOM() - Returns a random floating point number in the range [0,1]:: + Returns a random floating point number in the range [0..1]:: PRINT RANDOM(). //prints a random number + PRINT "Let's roll a 6-sided die 10 times:". + FOR n in range(0,10) { + + // To make RANDOM give you an integer in the range [0..n-1], you do this: + // floor(n*RANDOM()). + + // So for example : a die giving values from 1 to 6 is like this: + print (1 + floor(6*RANDOM())). + } .. function:: ROUND(a) diff --git a/doc/source/structures/celestial_bodies/atmosphere.rst b/doc/source/structures/celestial_bodies/atmosphere.rst index 8558cb328..b5f11e2a2 100644 --- a/doc/source/structures/celestial_bodies/atmosphere.rst +++ b/doc/source/structures/celestial_bodies/atmosphere.rst @@ -36,6 +36,21 @@ A Structure closely tied to :struct:`Body` A variable of type :struct:`Atmospher * - :attr:`HEIGHT` - :ref:`scalar ` (m) - advertised atmospheric height + * - :attr:`MOLARMASS` + - :ref:`scalar ` (kg/mol) + - The molecular mass of the atmosphere's gas + * - :attr:`ADIABATICINDEX` + - :ref:`scalar ` + - The Adiabatic index of the atmosphere's gas + * - :attr:`ADBIDX` + - :ref:`scalar ` + - Short alias for :attr:`ADIABATICINDEX` + * - :meth:`ALTITUDETEMPERATURE(altitude)` + - :ref:`scalar ` + - Estimate of temperature at the given altitude. + * - :meth:`ALTTEMP(altitude)` + - :ref:`scalar ` + - Short alias for :attr:`ALTITUDETEMPERATURE` .. attribute:: Atmosphere:BODY @@ -97,6 +112,42 @@ A Structure closely tied to :struct:`Body` A variable of type :struct:`Atmospher The altitude at which the atmosphere is "officially" advertised as ending. (actual ending value differs, see below). +.. attribute:: Atmosphere:MOLARMASS + + :type: :ref:`scalar ` + :acces: Get only + + The Molecular Mass of the gas the atmosphere is composed of. + Units are in kg/mol. + `Wikipedia Molar Mass Explanation `_. + +.. attribute:: Atmosphere:ADIABATICINDEX + + :type: :ref:`scalar ` + :access: Get only + + The Adiabatic index of the gas the atmosphere is composed of. + `Wikipedia Adiabatic Index Explanation `_. + +.. attribute:: Atmosphere:ADBIDX + + :type: :ref:`scalar ` + :access: Get only + + A shorthand alias for :attr:ADIABATICINDEX. + +.. method:: Atmosphere:ALTITUDETEMPERATURE(altitude) + + :parameter: altitude (:ref:`scalar `) the altitude to query temperature at. + :access: Get only + + Returns an approximate atmosphere temperature on this world at the given altitude. + Note that this is only approximate because the temperature will vary depending + on the sun position in the sky (i.e. your latitude and what time of day it is). + +.. method:: Atmosphere:ALTTEMP(altitude) + + A shorthand alias for :meth:ALTITUDETEMPERATURE(altitude). Deprecated Suffix ----------------- diff --git a/doc/source/structures/collections/lexicon.rst b/doc/source/structures/collections/lexicon.rst index 2198f1d11..8e4124597 100644 --- a/doc/source/structures/collections/lexicon.rst +++ b/doc/source/structures/collections/lexicon.rst @@ -74,6 +74,117 @@ the same effect as the previous code fragment:: The keys and the values of a lexicon can be any type you feel like, and do not need to be of a homogeneous type. +.. _lexicon_suffix: + +Lexicons can use suffix syntax +------------------------------ + +One special thing can be done with a :struct:`Lexicon` that cannot be done +with other types of structures in kOS - a lexicon can use the "suffix +syntax". + +By "suffix syntax", what is meant is things like the colons in these +statements:: + + print SHIP:VELOCITY. + print MUN:RADIUS. + +There is a special extra step when looking up a suffix. Normally +kOS throws an error if the suffix name refers to a suffix that does +not exist on the object. But, if the item on the left side of the +colon is a :struct:`LEXICON` type, then it also will check to see if +the suffix matches any of the lexicon's keys, and if it does, that +key's value will be retrieved. + +Here is an example:: + + local mylex is lexicon( + "key1", "value1", "key2", "value2", "key3", "value3"). + print mylex:key1. // prints "value1". + print mylex:key2. // prints "value2". + print mylex:key3. // prints "value3". + +This is added as a convenient shortcut. It literally means the same +thing as looking up the key with the square-bracket syntax:: + + local mylex is lexicon( + "key1", "value1", "key2", "value2", "key3", "value3"). + // These two lines are exactly the same: + print mylex["key1"]. + print mylex:key1. + +**The key must follow the rules for a valid identifier to do this:** + +Lexicons can use keys that are not even strings at all, but if you +want to use this suffix syntax, it will only work with string keys. +Furthermore, in order to use this shortcut, you must make sure the +string key you are trying to use is one that makes a valid identifier +in the kerboscript language. For example:: + + local mylex is lexicon( + "key_no_spaces", 100, "key with spaces", 200). + print mylex["key_no_spaces"]. // This works fine. + print mylex["key with spaces"]. // This works fine. + print mylex:key_no_spaces. // This works fine. + print mylex:key with spaces. // <-- BUT THIS IS AN ERROR. + +You cannot use a key as a suffix if that key has any characters in +it that make it invalid as an identifier, like spaces. This is +because the parser has to be able to read the colon suffix syntax +first before the system can start looking up the key value. + +This suffix syntax for lexicons only works because kerboscript is a +"late binding" language, where it doesn't try to find identifier names +until the moment it encounters them during the program run. Therefore +it can look up the lexicon names on the spot as it encounters that +line of code. + +In other words, this will cause an error:: + + local mylex is lexicon(). + print mylex:mykey. // <--- Error: no such thing in the lexicon yet. + sey mylex["mykey"] to "value". // here it gets added, but it's too late. + +While doing it in this order will work:: + + local mylex is lexicon(). + set mylex["mykey"] to "value". // adding the value first + print mylex:mykey. // makes this line work. + +Clashes between built-in suffixes versus lexicon keys +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +kOS will always prefer to use the built-in suffix name first when +trying to search for a suffix name in a lexicon. Therefore +if you make a key who's name matches an existing built-in suffix +term for Lexicons, you will get the built-in value instead of +your key's value. Here's an example:: + + local mylex is lexicon(). + set mylex["LENGTH"] to 20. + + // prints 1. LENGTH is already a suffix of Lexicons, so + // that's what this gets you, not the key called "length": + print mylex:length. + + // This will print 20, as there's no ambiguity that you were + // definitely looking for the key called "length" in this + // case, not the built-in suffix called "length": + print mylex["length"]. + +Suffix keys also work with HASSUFFIX and SUFFIXNAMES +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +All values in kerboscript derive from :ref:`Structure`, and +all such structures have :attr:`Structure:HASSUFFIX` and +:attr:`Structure:SUFFIXNAMES` members. Because a Lexicon has +this special ability to use the suffix syntax with keys, kOS +will add all the keys of a lexicon that are "suffix-able" to the +output of that lexicon's ``SUFFIXNAMES`` call. Also, when you +test if a suffix exists for a lexicon with ``HASSUFFIX``, any +key in that lexicon that could be used as a suffix will also +return true, in addition to the normal built-in suffixes. + Structure --------- @@ -123,6 +234,13 @@ Structure * - :meth:`REMOVE(keyvalue)` - None - removes the pair with the given key + * - :attr:`HASSUFFIX(name)` + - :struct:`Boolean` + - True if the suffix OR a key with the name, exists. + * - :attr:`SUFFIXNAMES` + - :struct:`List ` of :struct:`strings ` + - Gives both the suffixes AND the keys that work as suffixes + .. note:: This type is serializable. @@ -236,6 +354,28 @@ Structure Returns a List of the values stored in this lexicon. +.. method:: Lexicon:HASSUFFIX(name) + + :parameter name: :struct:`String` name of the suffix being tested for + :type: :struct:`Boolean` + :access: Get only + + This is just like the base method :meth:`Structure:HASSUFFIX(name)` that + all structures have, but with one slight difference - it will also return + true if the name you pass in matches one of the keys of this lexicon that + could be used with the :ref:`lexicon suffix syntax `. + +.. attribute:: Lexicon:SUFFIXNAMES + + :type: :struct:`List ` of :struct:`strings ` + :access: Get only + + All structures in kerboscript have a :attr:`Structure:SUFFIXNAMES` + attribute that shows a list of all the suffixes on the structure, + but for Lexicons the SUFFIXNAMES attribute has been altered so + that it will additionally include any keys of the suffix that could + be callled using the :ref:`lexicon suffix syntax `. + Access to Individual Elements ----------------------------- diff --git a/doc/source/structures/gui.rst b/doc/source/structures/gui.rst index 21667fe0f..68cd1ec60 100644 --- a/doc/source/structures/gui.rst +++ b/doc/source/structures/gui.rst @@ -31,6 +31,15 @@ call whenever you notice this particular thing has happened." For example:: will interrupt whatever else you are doing and call a function you wrote called ``myClickFunction`` whenever that button is clicked. +Note that gui callbacks that are triggered by user activity (rather than +by your program changing a value) are a type of trigger, and thus run at a +"higher priority" than normal code. You don't need to think too hard about +this right now, but the effect of it is that by default one gui callback +cannot trigger while another one is running. There are ways to change this +but they require a more in-depth discusion of how the kOS CPU works with +triggers, and are thus :ref:`described elsewhere on the +general CPU hardware description page`. + .. _gui_polling_technique: The **polling technique** is when you actively keep checking the widget diff --git a/doc/source/structures/misc/config.rst b/doc/source/structures/misc/config.rst index e9991454e..6e81d0d04 100644 --- a/doc/source/structures/misc/config.rst +++ b/doc/source/structures/misc/config.rst @@ -1,4 +1,4 @@ -.. config: +.. _config: Configuration of kOS ==================== @@ -104,6 +104,14 @@ Configuration of kOS - :struct:`Scalar` - 12 (from range [6 .. 20], integers only) - Default font size in pixel height for new instances of the in-game terminal + * - :attr:`DEFAULTWIDTH` + - :struct:`Scalar` + - 50 (from range [15 .. 255], integers only) + - Default width (in characters, not pixels) for new instances of the in-game terminal. + * - :attr:`DEFAULTHEIGHT` + - :struct:`Scalar` + - 36 (from range [3 .. 160], integers only) + - Default height (in characters, not pixels) for new instances of the in-game terminal. * - :attr:`DEBUGEACHOPCODE` - :struct:`Boolean` - false @@ -312,6 +320,34 @@ Configuration of kOS and no greater than 30 (very big). It will be rounded to the nearest integer when setting the value. +.. attribute:: Config:DEFAULTWIDTH + + :access: Get/Set + :type: :struct:`Scalar` integer-only. range = [15,255] + + Configures the ``TerminalDefaultWidth`` setting. + + This is the default starting width (in number of character cells, + not number of pixels) for all newly created kOS in-game terminals. + This is just the default for new terminals. Individual terminals + can have different settings, either by setting the value + :attr:`Terminal:WIDTH` in a script, or by manually dragging the + resize corner of the terminal with the mouse. + +.. attribute:: Config:DEFAULTHEIGHT + + :access: Get/Set + :type: :struct:`Scalar` integer-only. range = [3,160] + + Configures the ``TerminalDefaultHeight`` setting. + + This is the default starting height (in number of character cells, + not number of pixels) for all newly created kOS in-game terminals. + This is just the default for new terminals. Individual terminals + can have different settings, either by setting the value + :attr:`Terminal:HEIGHT` in a script, or by manually dragging the + resize corner of the terminal with the mouse. + .. attribute:: Config:DEBUGEACHOPCODE :access: Get/Set diff --git a/doc/source/structures/misc/time.rst b/doc/source/structures/misc/time.rst index fe5be4303..ac7f37bf6 100644 --- a/doc/source/structures/misc/time.rst +++ b/doc/source/structures/misc/time.rst @@ -75,6 +75,16 @@ Using a TimeSpan TIME:SECOND // 26 TIME:SECONDS // Total Seconds since campaign began + Note that the notion of "how many hours in a day" and "how many days in a year" + depends on the gameworld, not our real world. Kerbin has a shorter day, and + a longer year in days as a result, than Earth. But there is an option in + KSP's main settings screen that can toggle this notion, and kOS will use + whatever option you set it to. + + Also note that the mods that alter the calendar for other solar systems, + if they inject changes into KSP's main game, will cause these values to + change too. + .. highlight:: kerboscript Using TIME or TIME() to detect when the physics have been updated 'one tick' diff --git a/doc/source/structures/misc/vecdraw.rst b/doc/source/structures/misc/vecdraw.rst index de4ebb8cb..bba73c5c3 100644 --- a/doc/source/structures/misc/vecdraw.rst +++ b/doc/source/structures/misc/vecdraw.rst @@ -8,8 +8,8 @@ Drawing Vectors on the Screen to a new location, make it appear or disappear, change its color, and label. This page describes how to do that. -.. function:: VECDRAW(start, vec, color, label, scale, show, width) -.. function:: VECDRAWARGS(start, vec, color, label, scale, show, width) +.. function:: VECDRAW(start, vec, color, label, scale, show, width, pointy) +.. function:: VECDRAWARGS(start, vec, color, label, scale, show, width, pointy) Both these two function names do the same thing. For historical reasons both names exist, but now they both do the same thing. @@ -23,7 +23,9 @@ Drawing Vectors on the Screen "See the arrow?", 1.0, TRUE, - 0.2 + 0.2, + TRUE, + TRUE ). SET anArrow TO VECDRAWARGS( @@ -33,7 +35,9 @@ Drawing Vectors on the Screen "See the arrow?", 1.0, TRUE, - 0.2 + 0.2, + TRUE, + TRUE ). Vector arrows can also be created with dynamic positioning and color. To do @@ -52,7 +56,9 @@ Drawing Vectors on the Screen "Jumping arrow!", 1.0, TRUE, - 0.2 + 0.2, + TRUE, + TRUE ). wait 20. // Give user time to see it in motion. set anArrow:show to false. // Make it stop drawing. @@ -94,6 +100,10 @@ Drawing Vectors on the Screen - false * - :attr:`WIDTH` - 0.2 + * - :attr:`POINTY` + - true + * - :attr:`WIPING` + - true Examples:: @@ -102,7 +112,7 @@ Drawing Vectors on the Screen // paramters LABEL, SCALE, SHOW, and WIDTH. SET vd TO VECDRAW(V(0,0,0), 5*north:vector, red). - To make a :struct:`VecDraw` disappear, you can either set its :attr:`VecDraw:SHOW` to false or just UNSET the variable, or re-assign it. An example using :struct:`VecDraw` can be seen in the documentation for :func:`POSITIONAT()`. + To make a :struct:`VecDraw` disappear, you can either set its :attr:`VecDraw:SHOW` to false or just :ref:`UNSET ` the variable, or re-assign it. An example using :struct:`VecDraw` can be seen in the documentation for :func:`POSITIONAT()`. .. _clearvecdraws: @@ -157,6 +167,9 @@ Drawing Vectors on the Screen * - :attr:`WIDTH` - :ref:`scalar ` - width of vector, default is 0.2 + * - :attr:`POINTY` + - :ref:`boolean ` + - Will the pointy hat be drawn * - :attr:`STARTUPDATER` - :struct:`KosDelegate` - assigns a delegate to auto-update the START attribute. @@ -195,7 +208,13 @@ Drawing Vectors on the Screen :access: Get/Set :type: :ref:`Color ` - Optional, defaults to white. This is the color to draw the vector. There is a hard-coded fade effect where the tail is a bit more transparent than the head. + Optional, defaults to white. This is the color to draw the vector. + If you leave the :attr:`VecDraw:WIPING` suffix at its default value + of True, then there will be a wipe effect such that the line will + fade-in as it goes, only becoming this color at the endpoint tip. + + (You can pass in an RGBA with an alpha value less than 1.0 if you + would like the line to never be fully opaque even at the tip.) .. attribute:: VecDraw:COLOUR @@ -237,6 +256,34 @@ Drawing Vectors on the Screen left off. Note, this also causes the font of the label to be enlarged to match if set to a value larger than 0.2. +.. attribute:: VecDraw:POINTY + + :access: Get/Set + :type: :ref:`boolean ` + + (Defaults to True if left off.) Will this line be drawn with + a pointy arrowhead "hat" on the tip to show which end is the + start point and which is the end point? If this is false, + then Vecdraw draws just a thick line, instead of an arrow. + +.. attribute:: VecDraw:WIPING + + :access: Get/Set + :type: :ref:`boolean ` + + (Defaults to True if left off.) If true, this line will be drawn + with a "wipe" effect that varies how transparent it is. At the + start point it will be a more transparent version of the color + you specified in :attr:`VecDraw:COLOR`. It will only become the + full opacity you requested when it reaches the endpoint of the line. + This effect is to help show the direction the arrow is going as it + "fades in" to full opacity as it goes along. + + If false, then the opacity of the line will not vary. It will draw + the whole line at the exact color you specified in the in the + :attr:`VecDraw:COLOR` SUFFIX. (Which can still be transparent if + you use an RGBA() and provide the alpha value.) + .. attribute:: VecDraw:STARTUPDATER :access: Get/Set diff --git a/doc/source/structures/misc/versioninfo.rst b/doc/source/structures/misc/versioninfo.rst new file mode 100644 index 000000000..b04e9315f --- /dev/null +++ b/doc/source/structures/misc/versioninfo.rst @@ -0,0 +1,73 @@ +.. _versioninfo: + +A ``VersionInfo`` is a structure that breaks the version string for +kOS down into its component fields as numbers (rather than as +a string) so you can look at them one at a time. + +You obtain a :struct:`VersionInfo` by calling :attr:`CORE:VERSION`. + +Structure +--------- + +.. structure:: VersionInfo + + .. list-table:: Members + :header-rows: 1 + :widths: 2 1 1 4 + + * - Suffix + - Type + - Get/Set + - Description + + * - :attr:`MAJOR` + - :struct:`Scalar` + - get only + - The "NN" in the version string ``vNN.xx.xx.xx``. + + * - :attr:`MINOR` + - :struct:`Scalar` + - get only + - The "NN" in the version string ``vxx.NN.xx.xx``. + + * - :attr:`PATCH` + - :struct:`Scalar` + - get only + - The "NN" in the version string ``vxx.xx.NN.xx``. + + * - :attr:`BUILD` + - :struct:`Scalar` + - get only + - The "NN" in the version string ``vxx.xx.xx.NN``. + +.. attribute:: VersionInfo:MAJOR + + :access: Get only + :type: :struct:`Scalar`. + + The first number in the version string. i.e. the + "NN" in the version string ``vNN.xx.xx.xx``. + +.. attribute:: VersionInfo:MINOR + + :access: Get only + :type: :struct:`Scalar`. + + The second number in the version string. i.e. the + "NN" in the version string ``vxx.NN.xx.xx``. + +.. attribute:: VersionInfo:PATCH + + :access: Get only + :type: :struct:`Scalar`. + + The third number in the version string. i.e. the + "NN" in the version string ``vxx.xx.NN.xx``. + +.. attribute:: VersionInfo:BUILD + + :access: Get only + :type: :struct:`Scalar`. + + The fourth number in the version string. i.e. the + "NN" in the version string ``vxx.xx.xx.NN``. diff --git a/doc/source/structures/reflection/structure.rst b/doc/source/structures/reflection/structure.rst index d38cd6fd7..fa051cb4d 100644 --- a/doc/source/structures/reflection/structure.rst +++ b/doc/source/structures/reflection/structure.rst @@ -46,7 +46,7 @@ or ``42`` or ``"abc"``. For example, you can do:: - :struct:`String` - The string that gets shown on-screen when doing the PRINT command. - * - :attr:`HASSUFFIX(name)` + * - :meth:`HASSUFFIX(name)` - :struct:`Boolean` - Test whether or not this value has a suffix with the given name. @@ -80,7 +80,7 @@ or ``42`` or ``"abc"``. For example, you can do:: This suffix universally lets you get that string version of any item, rather than showing it on the screen. -.. attribute:: Structure:HASSUFFIX(name) +.. method:: Structure:HASSUFFIX(name) :parameter name: :struct:`String` name of the suffix being tested for :type: :struct:`Boolean` @@ -102,6 +102,11 @@ or ``42`` or ``"abc"``. For example, you can do:: and ":aaa" as being two different suffixes. In kerboscript, they'd be the same suffix. + (Note that because a :struct:`Lexicon` can use a special + :ref:`Lexicon suffix syntax `, it will also + return true for suffix-usable keys when you call its + HASSUFFIX method.) + .. attribute:: Structure:SUFFIXNAMES :type: :struct:`List ` of :struct:`strings ` @@ -142,6 +147,12 @@ or ``42`` or ``"abc"``. For example, you can do:: [12] = Y [13] = Z + (Note that because a :struct:`Lexicon` can use a special + :ref:`Lexicon suffix syntax `, it will also + include all of its suffix-usable keys when you call its + SUFFIXNAMES method.) + + .. attribute:: Structure:TYPENAME :type: :struct:`String` diff --git a/doc/source/structures/vessels/bounds.rst b/doc/source/structures/vessels/bounds.rst new file mode 100644 index 000000000..0179794c8 --- /dev/null +++ b/doc/source/structures/vessels/bounds.rst @@ -0,0 +1,807 @@ +.. _bounds: + +BOUNDS +====== + +kOS can return information about the "bounding box" around either a whole +vessel, or a part on a vessel. When it does so, the structure it uses +to tell your script about it is called a "Bounds". + +You can obtain a ``Bounds`` one of two ways: + +* By calling :attr:`Vessel:Bounds` +* By calling :attr:`Part:Bounds` + +The bounds for a whole vessel and the bounds for one part work almost +exactly the same way. Places where they differ will be explained below +as they arise. + +The reason this could be useful to a kerboscript program is that it lets +you get a partial "picture" of the size of a ship, or a part, or check +more precisely when landing "*what is the altitude of my landing leg's foot +above the ground?*" instead of "what is the altitude of my ship in general +above the ground?" + +Before you use a Bounds be certain you have read the section +below on this page about :ref:`why you should re-use bounds +if you can ` (instead of re-getting them again and again +with the :BOUNDS suffix call.) + +Quick start 1 - Radar altitude: the most useful thing +----------------------------------------------------- + +The explanation of the "Bounds" structure might get a bit involved below. + +For most players, probably the only thing you really really care deeply +about is the distance of the landing legs to the ground. + +And you get that with the suffix :attr:`BOTTOMALTRADAR`. Here's an example:: + + // How to get the distance of the bottom corner of the vessel's + // bounding box to the ground. + // As an example, try running this while you manually land, + // so you can see it working as you fly the lander: + + local bounds_box is ship:bounds. // IMPORTANT! do this up front, not in the loop. + until terminal:input:haschar { + clearscreen. + print "PRESS ANY KEY TO QUIT". + print "ALT:RADAR of ship bounding box's bottom corner is:". + print round(bounds_box:BOTTOMALTRADAR, 1). + wait 0. + } + terminal:input:getchar(). + +It may be more useful to choose a specific landing leg part and test with it +instead of the whole vessel's box, by doing something akin to this at the +top of the above script instead:: + + // DELETE THIS LINE: + // local bounds_box is ship:bounds. // IMPORTANT! do this up front, not in the loop. + // + // REPLACE WITH THESE LINES: + // Assumes you gave the nametag "sensing leg" to one of your landing legs: + // + set leg_part to ship:partstagged("sensing leg")[0]. + local bounds_box is leg_part:bounds. + + // (rest of the example is the same as above). + +Quick start 2 - Finding the corner you care about +------------------------------------------------- + +The most universally helpful suffix of a ``Bounds``, if you want +to just learn one suffix and ignore the others, is probably the +:meth:`Bounds:FURTHESTCORNER(ray)` suffix. + +It lets you say "I just want to know where is the corner of +this bounding box that is the furthest 'that way'". + +For example, this gives you a position which is the bottom-most +vertex of your ship's bounding box:: + + set B to ship:BOUNDS. + + // The furthest corner of the box in the downward (negative up) direction: + set bottom to B:FURTHESTCORNER( - up:vector ). + +With this one suffix you can answer a lot of the relevant "ship size" +questions like "Am i getting too close to that ship?, well, lets find +out which vertex of its bounding box is closest to me...". + +Quick Start 3 - A complex visual example drawing the bounds +----------------------------------------------------------- + +There is an example program in the tutorials section that +brings this all together to show you the bounds boxes +visually on screen: + +:ref:`Click here for an example program that displays bounds ` + +Trying that first (without necessarily understaand it right away) +will help give you a visual guide to what is happening here. + +A Part:BOUNDS or Vessel:BOUNDS will move and rotate with the object +------------------------------------------------------------------- + +If you get a ``Bounds`` from calling either :attr:`Part:BOUNDS` +or :attr:`Vessel:BOUNDS`, then that "bounds" is magically tied +to the vessel or part it came from. + +Internally, kOS is doing some "magic" to ensure that the +``Bounds`` structure "remembers" the part, or vessel, that it is +associated with, and keeps itself updated to that part or +vessel's new moved position and orientation. This means +the values for the "absolute" suffixes described in the +table below (namely :attr:`ABSMIN`, :attr:`ABSMAX`, +:attr:`ABSCENTER`, :attr:`ABSORIGIN`, :attr:`FACING`, +:attr:`FURTHESTCORNER`, :attr:`BOTTOMALT`, and :attr:`BOTTOMALTRADAR`) +will always be kept up to date every time you get their value +it will be newly calculated and correct even though +the part or vessel has been rotating or moving since you +last called the ``:BOUNDS`` suffix. + +SETTING a suffix of Bounds can break the link to its object +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This "magically keep updating things" is only guaranteed to +keep happening if you restrict yourself to only using GET access +on Bounds suffixes. If you ever SET the :attr:`ABSORIGIN` +or :attr:`FACING` suffixes to some other value, then Bounds will +no longer keep updating that suffix to match the object it came from. +(and consequently the other suffixes won't be updating themselves +properly either, as they depend on calculations from these two). +This is not a bug. It's intentional. When you SET a suffix of a +Bounds, you are explicitly telling it to use your new value +instead of its usual practice of always re-calculating it from +the part or vessel it came from. + +.. _reuse_bounds: + +A Bounds structure is re-usable. Please do re-use it. +------------------------------------------------------ + +While it may seem like these two examples below are the same, the second +example is MUCH less of a burden on the KSP game than the first one:: + + // Example 1: An expensive example using :BOUNDS again and again: + // (THIS IS A BAD PRACTICE): + // Please set the ship rotating before doing this, to prove that + // it is indeed seeing the new rotated positions: + // + print "100 samples of my min/max corners as I rotate:". + for i in range(0,100) { + print i + ": absmin=" + SHIP:bounds:absmin + ", absmax=" + SHIP:bounds:absmax. + wait 0. + } + +:: + + // Example 2: The exact same thing, done more efficiently: + // (THIS IS BETTER PRACTICE): + // Please set the ship rotating before doing this, to prove that + // it is indeed seeing the new rotated positions: + // + local B is SHIP:bounds. // get the :bounds suffix ONCE. + print "100 samples of my min/max corners as I rotate:". + for i in range(0,100) { + print i + ": absmin=" + B:absmin + ", absmax=" + B:absmax. + wait 0. + } + +The reason example 1 is more expensive is that every time you call +the :bounds suffix, you make kOS under the hood re-run some coordinate +transformations, and the ``Bounds`` structure is explicitly designed +to make it so you don't have to keep doing that to make it accurate. +It "remembers" which object's orientation it needs to be using, and +it keeps re-correcting itself to that objects orientation for you +every time you use it. + +The expense of calling ``Part:BOUNDS`` isn't that bad and calling it +repeatedly probably won't really make your script suffer noticably. +But when you do it for the whole vessel, calling ``Vessel:BOUNDS`` +repeatedly, that can definitely result in noticable unnecessary +computations being done by your computer. + +For a more in-depth explanation of why it's expensive to re-call +the Bounds suffix over and over, if you care, see +:ref:`The bottom of this page `. For now, it is +sufficient to say "it's expensive, don't do it". + + +.. _bounds_invalidate: + +Things that will invalidate an existing Bounds +---------------------------------------------- + +As explained elsewhere on this page, it is much faster +and less taxing on the KSP game to re-use a ``Bounds`` +instead of obtaining a new one. The ``Bounds`` object +contains some internal logic to track rotation and movement +of the ship so the bounds box will rotate properly with it, +and the bounds boxes of individual parts will rotate if +the part rotates. + +However, be aware of these following situations that can +cause a previously - obtained ``Bounds`` to become incorrect, +and require you to get a new Bounds with the suffix. Because +doing so is expensive, don't fall to the temptation of just +making your script easy to write by unconditionally re-getting +the ``Bounds`` suffix all the time. Instead be aware of what +makes you have to get a new Bounds, and don't do so if these +events aren't happening: + +A list of events that can make a ``Bounds`` become incorrect: + +* A Part Bounds will need to be recalculated if the part shrinks + or grows through actions such as these: + + * Extending or retracting solar panels. + * Extending or retracting Landing Gear. + * Opening or closing cargo bay doors. + * Moving robotic parts from the Breaking Ground DLC. + * etc. + +* A Vessel Bounds will need to be recalculated if any Part Bounds + inside the vessel needs to be recalculated (see above list). + In addition, the items on the following list will require a + Vessel's Bounds (but not individual parts' bounds) to be + recaculated: + + * Anything that adds/removes parts obviously alters the + bounding box of the vessel. These are examples but not an + exhuastive list: + + * Docking and Undocking + * Decoupling stages + * Explosions + * Using the asteroid grabber claw. + + * Anything that changes the vessel's "control" orientation. + (As in anything that makes the navball jump to a new + orientation all at once). That invalidates the old bounding box + because it swaps the meaning of which axis of the ship is + the "fore" and which is the "starboard" and so on. These are + examples but not an exhaustive list: + + * Right-clicking a docking port and saying "control from here". + * Right-clicking a lander can and choosing a new control orientation. + * Entering IVA view (which has the side effect of making the game + do a "control from here" on the cockpit part). + +Also, be aware that getting a new :attr:`Part:BOUNDS` is a LOT +less expensive than getting a whole new :attr:`Vessel:BOUNDS`, +so if your script task does need to constantly re-get the +bounds, try writing it in such a way that it only needs to +re-get the bounds of one or two parts, not the whole vessel. +(For example, for a landing script, maybe try to have your script learn +which part of your vessel is the bottom-most part you'll be landing on, +and just use that one part's bounds to test the height to the ground +instead of the entire vessel's bounds.) + + +Making your own Bounds +---------------------- + +There are a few suffixes of Bounds that are settable. +Doing so isn't very useful for Bounds coming from the vessel +or a part. But the reason they are settable is so you can make +your own bounds objects if you feel the need to. + +At minimum to make your own bounds you will need these pieces +of information: + + * The ABSORIGIN of the bounds. + * The FACING of the bounds. + * The RELMIN of the bounds. + * The RELMAX of the bounds. + +The following function will let you construct your own Bounds, +although it's not clear what use this would have yet:: + + local my_bounds is BOUNDS( ABSORIGIN, FACING, RELMIN, RELMAX ). + +The other suffixes are derived from calculations based on these. + +Example:: + + + // Makes a bounds that is centered around a flag, + // oriented in that flag's UP direction, which + // goes a lot further up into the sky than it does down + // into the ground (to demonstrate that the bounds box + // doesn't have to span equally far in all directions + // around the origin, and thus why the origin isn't always + // the center of the box): + local my_flag is vessel("that flag"). + local my_bounds is BOUNDS( + my_flag:position, + my_flag:up, // In this facing, Z = up/down, X = north/south, and Y = east/west. + + // box is 20x20x502 meters, centered in east/west/north/south terms, but + // extending higher up in the +Z direction than down in the -Z direction: + V(-10,-10, -2), + V(10, 10, 500) + ). + +Again, it's unclear how a script might use this, but it's there +for completeness. + +Obviously, a bounds box you make manually yourself this way does not +have the "magic" linkage to a vessel or part that the ones kOS makes have, +and therefore its position is more fixed in space unless your script +manually re-assignes its properties. + +Diagram +------- + +When looking at the suffix explanations below, these diagrams may help +illustrate what is being talked about: + +.. figure:: /_images/structures/vessels/bounding_vessel.png + :alt: Showing bounding box around a Vessel + + What some of the terms mean for a bounding box around a vessel. + + +.. figure:: /_images/structures/vessels/bounding_part.png + :alt: Showing bounding box around a Part + + What some of the terms mean for a bounding box around a part. + +.. structure:: Bounds + + .. list-table:: + :header-rows: 1 + :widths: 2 1 1 4 + + * - Suffix + - Type + - Access + - Description + + * - :attr:`ABSORIGIN` + - :struct:`Vector` + - Get/Set + - origin point of box, in absolute ship-raw coords. + * - :attr:`FACING` + - :struct:`Direction`` + - Get/Set + - The orientation of the box's own reference frame. + * - :attr:`RELMIN` + - :struct:`Vector` + - Get/Set + - a corner of the box in box's own reference frame. + * - :attr:`RELMAX` + - :struct:`Vector` + - Get/Set + - opposite corner of the box from RELMIN, in box's own reference frame. + * - :attr:`ABSMIN` + - :struct:`Vector` + - Get only + - a corner of the box in absolute (ship-raw) reference frame. + * - :attr:`ABSMAX` + - :struct:`Vector` + - Get only + - opposite corner of the box from RELMIN, in absolute (ship-raw) reference frame. + * - :attr:`ABSCENTER` + - :struct:`Vector` + - Get only + - center of the box (not its origin), in absolute (ship-raw) frame. + * - :attr:`RELCENTER` + - :struct:`Vector` + - Get only + - center of the box (not its origin), in box's own reference frame. + * - :attr:`EXTENTS` + - :struct:`Vector` + - Get/Set + - A vector from box center to max corner, in box's reference frame. + * - :attr:`SIZE` + - :struct:`Vector` + - Get/Set + - Exactly 2 times EXTENTS - the vector from min corner to max, in box's reference frame. + * - :meth:`FURTHESTCORNER(Vector ray)` + - :struct:`Vector` + - Get only + - Position (in absolute ship-raw coords) of the box corner most "that-a-way". + * - :attr:`BOTTOMALT` + - :ref:`Scalar` + - Get Only + - Sea-level altitude of bottom-most corner of box. + * - :attr:`BOTTOMALTRADAR` + - :ref:`Scalar` + - Get Only + - Radar altitude of bottom-most corner of box. + * - RELORIGIN is missing + - n/a + - DOES NOT EXIST + - This suffix is deliberately missing because it would always be V(0,0,0). + +.. attribute:: Bounds:ABSORIGIN + + :type: :struct:`Vector` + :access: Get/Set but read the note below before you SET it. + + The position of the origin point of the bounding box, expressed + in absolute coordinates (what kOS calls the ship-raw reference + frame, that the rest of the position vectors in kOS use.) + + If this bounding box came from a Part, this will be the same as + that part's ``Part:POSITION``, and will keep being "magically" + updated to stay with that part's position if it moves or rotates + (but see note below). + + If this bounding box came from a vessel, this will be the same as + that vessel's ``Vessel:PARTS[0]:POSITION`` (the position of its + root part) and be "magically" updated to stay with that part's + position if it moves or rotates (but see note below). It is + NOT ``Vessel:position``, which is important. ``Vessel:position`` + is the *center of mass* of a vessel. While kOS prefers to use + CoM as the official position of a vessel most of the time, the fact + that using fuel shifts the position of the CoM within the vessel made + it impractical to use CoM for the vessel's bounding box origin. + + **WARNING about using SET with this suffix:** *If this bounds box + was obtained using :attr:`Part:BOUNDS` or :attr:`Vessel:BOUNDS`, + then this suffix keeps changing its value to remain correct as the + vessel rotates or moves. But ONLY if you restrict your use of this + suffix to GET-only. If you ever SET this suffix, kOS stops that + auto-updating so it won't override the value you gave. Generally, + using SET on this suffix was only ever intended for Bounds you + created manually with the BOUNDS() function.* + +.. attribute:: Bounds:RELORIGIN + + :type: Nonexistent + :access: Nonexistent + + **This suffix does not exist**. It is mentioned here simply because you + might be trying to look up a suffix with this name, thinking + it should exist, wondering "well, there's an ABSMIN and a + RELMIN, an ABSCENTER and a RELCENTER... where's the RELORIGIN that + should go with ABSORIGIN?". + + The reason there is no RELORIGIN is that given how a ``Bounds`` stores + values, by its very definition the RELORIGIN would be V(0,0,0), always. + It's the origin of the bounding box, in the bounding box's own reference + frame - a reference frame with this spot as its origin. + +.. attribute:: Bounds:FACING + + :type: :struct:`Direction` + :access: Get/Set but read the note below before you SET it. + + This defines the orientation of this bounding box's local + reference frame, by providing a rotation that will get you + from the bounding-box relative orientation (in which the + X, Y, and Z axes are parallel to the bounding box's edges) + to the absolute orientation (the ship-raw orientation the + rest of kOS uses). + + If this bounding box came from a Part, this will be the same as + that part's ``Part:FACING``, and will keep being "magically" + updated to stay aligned with that part's facing if it moves or + rotates (but see note below). + + If this bounding box came from a Vessel, this will be the same as + that Vessel's ``Vessel:FACING``, and will keep being "magically" + updated to stay aligned with that part's facing if it moves or + rotates (but see note below). + + **WARNING about using SET with this suffix:** *If this bounds box + was obtained using :attr:`Part:BOUNDS` or :attr:`Vessel:BOUNDS`, + then this suffix keeps changing its value to remain correct as the + vessel rotates or moves. But ONLY if you restrict your use of this + suffix to GET-only. If you ever SET this suffix, kOS stops that + auto-updating so it won't override the value you gave. Generally, + using SET on this suffix was only ever intended for Bounds you + created manually with the BOUNDS() function.* + +.. attribute:: Bounds:RELMIN + + :type: :struct:`Vector` **in bounding-box relative reference frame** + :access: Get/Set + + A vector expressed in the bounding-box-relative reference frame + (where the XYZ axes are parallel to the bounding box's edges). + + This defines one corner of the bounding box. It is the + "negative-most" corner of the box. If you drew a vector from + the box's origin point to its "negative-most" corner, that would + be this vector. By "negative-most" that simply means the corner + where the X, Y, and Z coordinates have their smallest values. + (again, in the bounding box's own reference frame, not the absolute + world (ship-raw) frame.) + + This corner will always be the diagonally opposite corner from + :attr:`Bounds:RELMAX`. + + If you SET this value, you are changing the size of the + bounding box, making it larger (or smaller), as well as + stretching or shrinking it, depending on the new value + you pick. Doing so doesn't *actually* change the size of + a part or vessel, and is really only useful if you are + working with your own ``Bounds`` you created manually with + the ``Bounds()`` built-in function. + + Be careful when trying to "add" the RELMIN vector to other + vectors in the game. It's not oriented in ship-raw coords. + To rotate it into ship-raw coords you can multiply it by + the bounds facing like so: ``MyBounds:FACING * MyBounds:RELMIN``. + +.. attribute:: Bounds:RELMAX + + :type: :struct:`Vector` **in bounding-box relative reference frame** + :access: Get/Set + + A vector expressed in the bounding-box-relative reference frame + (where the XYZ axes are parallel to the bounding box's edges). + + This defines one corner of the bounding box. It is the + "positive-most" corner of the box. If you drew a vector from + the box's origin point to its "positive-most" corner, that would + be this vector. By "positive-most" that simply means the corner + where the X, Y, and Z coordinates have their greatest values. + (again, in the bounding box's own reference frame, not the absolute + world (ship-raw) frame.) + + This corner will always be the diagonally opposite corner from + :attr:`Bounds:RELMIN`. + + If you SET this value, you are changing the size of the + bounding box, making it larger (or smaller), as well as + stretching or shrinking it, depending on the new value + you pick. Doing so doesn't *actually* change the size of + a part or vessel, and is really only useful if you are + working with your own ``Bounds`` you created manually with + the ``Bounds()`` built-in function. + + Be careful when trying to "add" the RELMAX vector to other + vectors in the game. It's not oriented in ship-raw coords. + To rotate it into ship-raw coords you can multiply it by + the bounds facing like so: ``MyBounds:FACING * MyBounds:RELMAX``. + +.. attribute:: Bounds:ABSMIN + + :type: :struct:`Vector` + :access: Get + + This is the same point as :attr:`Bounds:RELMIN`, except it has + been rotated and translated into absolute coordinates (what + kOS calls the ship-raw reference frame, that the rest of the + position vectors in kOS use.) + + You cannot SET this value, because it is generated + from the ABSORIGIN, the FACING, and the RELMIN. + + Calculating the ABSMIN could be done in kerboscript from the + other Bounds suffixes (see example below), but this is provided + for convenience:: + + // The following two print lines should print + // the same vector, within reason. (There may be a + // small floating point precision variance between them): + set B to ship:bounds. + print B:ABSMIN. + print B:ABSORIGIN + (B:FACING * B:RELMIN). + + +.. attribute:: Bounds:ABSMAX + + :type: :struct:`Vector` + :access: Get + + This is the same point as :attr:`Bounds:RELMAX`, except it has + been rotated and translated into absolute coordinates (what + kOS calls the ship-raw reference frame, that the rest of the + position vectors in kOS use.) + + You cannot SET this value, because it is generated + from the ABSORIGIN, the FACING, and the RELMAX. + + Calculating the ABSMAX could be done in kerboscript from the + other Bounds suffixes (see example below), but this is provided + for convenience:: + + // The following two print lines should print + // the same vector, within reason. (There may be a + // small floating point precision variance between them): + set B to ship:bounds. + print B:ABSMAX. + print B:ABSORIGIN + (B:FACING * B:RELMAX). + +.. attribute:: Bounds:RELCENTER + + :type: :struct:`Vector` **in bounding-box relative reference frame** + :access: Get-only + + The center of the bounding box, in its own relative reference frame. + (Not the absolute ship-raw reference frame the rest of kOS uses.) + + This is the offset between the bounding box's origin and its center. + + The origin of a bounding box is often not at its center because a + bounding box can extend further in one direction than the other. + For example a vessel's root part is often up at the top of the rocket, + such a vessel's bounding box will extend much further in the "aft" + direction than it does in the "fore" direction. The wing parts in the + game are often defined with their origin point at the base where they + glue to the fuselage, not out in the middle of the wing. + + Instead of being provided directly, this value could be calculated + from the RELMIN and RELMAX. It's simply the point exactly halfway + between those two opposite corners. + +.. attribute:: Bounds:ABSCENTER + + :type: :struct:`Vector` + :access: Get-only + + This is just the same thing as :attr:`Bounds:RELCENTER`, but + in the absolute (ship-raw) reference frame which scripts might find + more useful. + + It's exactly equivalent to doing this:: + + MyBounds:ABSORIGIN + (MyBounds:FACING * MyBounds:RELCENTER). + + Instead of being provided directly, this value could be calculated + from the ABSMIN and ABSMAX. It's simply the point exactly halfway + between those two opposite corners. + +.. attribute:: Bounds:EXTENTS + + :type: :struct:`Vector` **in bounding-box relative reference frame** + :access: Get-only + + A vector (in bounding-box relative reference frame, NOT the + absolute (ship-raw) reference frame the rest of kOS uses) + that describes where :attr:`Bounds:RELMAX` is, relative to + to the box's center (rather than to its origin). + + Note that the vector in the inverse direction of this one (that you'd + get by multiplying it by -1), points from the center to + the oppposite corner, the :attr:`Bounds:RELMIN`. + +.. attribute:: Bounds:SIZE + + :type: :struct:`Vector` **in bounding-box relative reference frame** + :access: Get-only + + A vector (in bounding-box relative reference frame, NOT the + absolute (ship-raw) reference frame the rest of kOS uses) + that describes the ray from RELMIN to RELMAX that goes diagonally + across the whole box. It's always just the same thing you'd + get if you took the :attr:`Bounds:EXTENTS` vector and multiplied + it by the scalar 2. + +.. method:: Bounds:FURTHESTCORNER(ray) + + :parameter ray: The "that-a-way" :struct:`Vector` in absolute (ship-raw) reference frame. + :return: :struct:`Vector` in absolute (ship-raw) referece frame. + + Returns the position (in absolute (ship-raw) reference frame) of + whichever of the 8 corners of this bounding box is "furthest" in + the direction the ray vector is pointing. Useful when you want + to know the furthest a bounding box extends in some gameworld + direction. + + Examples:: + + // Assume other_vessel has been set to some vessel nearby + // other than the current SHIP: + // + local ves_box is other_vessel:bounds. + local top is ves_box:furthestcorner(up:vector). + local bottom is ves_box:furthestcorner(-up:vector). + + local from_other_to_me is ship:position - other_vessel:position. + local nearest is ves_box:furthestcorner(from_other_to_me). + print "The closest point on the other vessel's bounds box is this far away:". + print nearest:mag. + + // A more complex example showing how you might use bounds boxes + // when trying to figure out how big a target vessel is so you + // know how to go around it: + // + local my_left is -ship:facing:starvector. + local leftmost is ves_box:furthestcorner(my_left). + print "In order to go around the other vessel, to the left, ". + pritn "I would need to shift myself this far to my left:". + print vdot(-ship:facing:starvector, leftmost). + +.. attribute:: Bounds:BOTTOMALT + + :type: :ref:`Scalar` + :access: Get-only + + The above-sea-level altitude reading from the bottom-most corner of + this bounding box, toward whichever Body the current CPU vessel is + orbiting. + + Note that it's always using the CPU vessel's *current* + body to decide which body is the one that defines the bounding + box's "downward" direction for picking its bottom-most corner, + and it uses that same body to decide what counts as "altitude", + regardless of wether the bounds box is a bounds box of the current + CPU vessel or something else. + + To put it another way: You can't "read" what the altitude of a + bounding box above the Mun is if your ship is currently + in Kerbin's sphere of influence. If you are currently orbiting + Kerbin, it will assume that the "bottom" of any Bounds box you + refer to means "corner closest to Kerbin" and "altitude" means + "distance from Kerbin's Sea level". Once your CPU vessel moves + into the Mun's sphere of influence this will change it it will + now assume that the "bottom" of a Bounds is the corner closest + to the Mun and the altitude you care about is the altitude above + the Mun. + + This may seem like a limitation, but it really isn't, since you + wouldn't be able to query a vessel or a part for its bounding + box if that vessel was far enough away to be outside the loading + distance and thus its full set of parts isn't "there". + It would only be a limitation for cases where you are inventing + your own bounds boxes from scratch. + +.. attribute:: Bounds:BOTTOMALTRADAR + + :type: :ref:`Scalar` + :access: Get-only + + The radar-altitude reading from the bottom-most corner of + this bounding box, toward whichever Body the current CPU vessel is + orbiting. Same as :attr:`Bounds:BOTTOMALT` except for the + difference between above-sea-level altitude versus radar altitude. + + +Side topic - what is a bounding box and how does kOS know about it? +------------------------------------------------------------------- + +A bounding box is a rectangular box around an item that represents the +smallest space that contains all verteces of the item yet is still +shaped like a box. It is common in graphics and video games for the +GPU and/or game engine to maintain information about the bounding boxes +of items in the game. Knowing this information is part of what they +do to reduce their large workload. When checking if two items are +colliding, or checking if part of object A blocks the line of sight +between the camera and part of object B it's trying to draw, a check +against the bounding box first is quick and simple. If the thing +you're checking isn't even intersecting the bounding box, then it's +impossible for it to be a hit on the actual complex shape inside it. +The expensive check that looks at the exact shape of the object's +mesh can be skipped when you're not even intersecting the bounding +box around it. + +.. _bounds_expense: + +Why is calling :bounds repeatedly a slow thing to do? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The bounding box Unity tracks for items is aligned with the XYZ axes +of the game's world coordinates, what kOS calls the "ship-raw" reference +frame. This is vital for the game engine's needs, as the speedy quick +bounding box intersection tests work by doing simple greater-than +and less-than tests of the coordinates. This means the box will also be +"too big" if the item is rotated from the world's XYZ axes, as the +world-aligned box has to accomodate the item's "diagonal" corners pushing +the box bigger. For the purposes of a graphics engine, that's fine, since +erring on the side of a too-big bounding box is okay, since it's nothing +more than a time savings to short-circuit work, and not the final say-so +on whether item A is touching item B. + +To get a rotated box like is needed for kOS's needs, where its tightly +snug against the object in question, kOS has to go a bit more low-level +and look at the actual meshes that make up the object, and look at +*their* bounding boxes, which are aligned in the mesh's own locally +rotated XYZ axes, rather than world axes. Some ugly transforms +of each of the 8 vertices of the mesh bounding box Unity knows about are +needed, and there isn't really a good way to do this without running a +loop across all vertices of the box, which is what kOS does internally. +For the whole vessel's bounding box, this means doing those 8 vertices +per part, on every part on the ship. + +Why then is it faster to re-use the BOUNDS suffix? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Internally, kOS stores the *relative* bounds *in* the reference +frame of the object (the *facing* suffix), and uses a delegate to +keep getting the new orientation and origin of that object every +time you ask for a Bounds' suffix. Thus it doesn't have to keep +re-doing the transformations described above as the part rotates +or shifts. It only has to apply the rotation and translation to +the existing relative bounds it already calculated the expensive +way the first time. + +Credit to kRPC +~~~~~~~~~~~~~~ +The messy problem described above wasn't solved until peeking at the +code inside the kRPC mod, which also offers bounding box information. +That peek showed a hint at the similar steps kOS would need to do to +support the same thing, and gave an explanation why the "raw" bounding +box that Unity gave was always too big and seemed wrong. That led to a +lot of looking at answers on the Unity forum from other people who had +the same problem, and did similar solutions to what kRPC was doing, +indicating that despite looking like a lot of work, this was really +the only way to do it. (Since kRPC and kOS are both GPL3, this peeking +is totally legit.) diff --git a/doc/source/structures/vessels/core.rst b/doc/source/structures/vessels/core.rst index f0fef82a4..acbe44249 100644 --- a/doc/source/structures/vessels/core.rst +++ b/doc/source/structures/vessels/core.rst @@ -52,7 +52,7 @@ Core represents your ability to identify and interact directly with the running .. attribute:: CORE:VERSION - :type: `VersionInfo` + :type: :struct:`VersionInfo` :access: Get only The kOS version currently running. diff --git a/doc/source/structures/vessels/part.rst b/doc/source/structures/vessels/part.rst index 52e18f0a3..512a966b7 100644 --- a/doc/source/structures/vessels/part.rst +++ b/doc/source/structures/vessels/part.rst @@ -54,6 +54,9 @@ These are the generic properties every PART has. You can obtain a list of values * - :attr:`FACING` - :struct:`Direction` - the direction that this part is facing + * - :attr:`BOUNDS` + - :struct:`Bounds` + - Bounding-box information about this part's shape * - :attr:`RESOURCES` - :struct:`List` - list of the :struct:`Resource` in this part @@ -211,7 +214,31 @@ These are the generic properties every PART has. You can obtain a list of values :access: Get only :type: :struct:`Direction` - the direction that this part is facing. + The direction that this part is facing, which is also the rotation + that would transform a vector from a coordinate space where the + axes were oriented to match the part, to one where they're + oriented to match the world's ship-raw coordinates. + +.. attribute:: Part:BOUNDS + + :access: Get only + :type: :struct:`Bounds` + + Constructs a "bounding box" structure that can be used to + give your script some idea of the extents of the part's shape - how + wide, long, and tall it is. + + It can be slightly expensive in terms of CPU time to keep calling + this suffix over and over, as kOS has to perform some work to build + this structure. If you need to keep looking at a part's bounds again + and again in a loop, and you know that part's shape isn't going to be + changing (i.e. you're not going to extend a solar panel or something + like that), then it's better for you to call this ``:BOUNDS`` suffix + just once at the top, storing the result in a variable that you use in + the loop. + + More detailed information is found on the documentation page for + :struct:`Bounds`. .. attribute:: Part:MASS diff --git a/doc/source/structures/vessels/vessel.rst b/doc/source/structures/vessels/vessel.rst index 22d636b09..0560ab352 100644 --- a/doc/source/structures/vessels/vessel.rst +++ b/doc/source/structures/vessels/vessel.rst @@ -34,6 +34,7 @@ Vessels are also :ref:`Orbitable`, and as such have all the associate :attr:`AVAILABLETHRUST` :struct:`scalar` Sum of active limited maximum thrusts :meth:`AVAILABLETHRUSTAT(pressure)` :struct:`scalar` Sum of active limited maximum thrusts at the given atmospheric pressure :attr:`FACING` :struct:`Direction` The way the vessel is pointed + :attr:`BOUNDS` :struct:`Bounds` Construct bounding box information about the vessel :attr:`MASS` :struct:`scalar` (metric tons) Mass of the ship :attr:`WETMASS` :struct:`scalar` (metric tons) Mass of the ship fully fuelled :attr:`DRYMASS` :struct:`scalar` (metric tons) Mass of the ship with no resources @@ -144,7 +145,42 @@ Vessels are also :ref:`Orbitable`, and as such have all the associate :type: :struct:`Direction` :access: Get only - The way the vessel is pointed. + The way the vessel is pointed, which is also the rotation + that would transform a vector from a coordinate space where the + axes were oriented to match the vessel's orientation, to one + where they're oriented to match the world's ship-raw coordinates. + + i.e. ``SHIP:FACING * V(0,0,1)`` gives the direction the + ship is pointed (it's Z-axis) in absolute ship-raw coordinates + +.. attribute:: Vessel:BOUNDS + + :type: :struct:`Bounds` + :access: Get only + + Constructs a "bounding box" structure that can be used to + give your script some idea of the extents of the vessel's shape - how + wide, long, and tall it is. + + It is rather expensive in terms of CPU time to call this suffix. + (Calling :attr:`Part:BOUNDS` on ONE part on the ship is itself a + *little* expensive, and this has to perform that same work on + every part on the ship, finding the bounding box that would + surround all the parts.) Because of that expense, kOS **forces** + your script to give up its remaining instructions this update when + you call this (It forces the equivalent of doing a ``WAIT 0.`` + right after you call it). This is to discourage you from + calling this suffix again and again in a fast loop. The proper + way to use this suffix is to call it once, storing the result in + a variable, and then use that variable repeatedly, rather than + using the suffix itself repeatedly. Only call the suffix again + when you have reason to expect the bounding box to change or + become invalid, such as docking, staging, changing facing to a + new control-from part, and so on. + + More detailed information about how to read the bounds box, and + what circumstances call for getting a re-generated copy of the + bounds box, is found on the documentation page for :struct:`Bounds`. .. attribute:: Vessel:MASS diff --git a/doc/source/tutorials.rst b/doc/source/tutorials.rst index 42d667a54..91eb5d635 100644 --- a/doc/source/tutorials.rst +++ b/doc/source/tutorials.rst @@ -13,6 +13,7 @@ If you prefer the tutorial style of explanation, please see the following exampl Design Patterns PID Loops Execute Node script + Drawing VecDraws, and showing Bounds Boxes Making a tabbed GUI Introductory @@ -36,6 +37,12 @@ Intermediate :doc:`Execute Node script ` ZiwKerman describes a generic "execute manuever node" script to be a one-size-fits-all solution to many situations in KSP. If you can make a manuever node for something, exenode will execute it. +:doc:`Display Bounds script ` + An example program showing how to read vessel and part bounding + boxes to learn what the shape of the ship is. It also contains some + rotation transformation of vectors, and some examples of drawing + lines and vectors with VECDRAW. + :doc:`Creating Reusable GUI Elements ` Creating and using a "TabWidget" to put a lot of functionality in a small amount of screenspace. diff --git a/doc/source/tutorials/display_bounds.rst b/doc/source/tutorials/display_bounds.rst new file mode 100644 index 000000000..8be147748 --- /dev/null +++ b/doc/source/tutorials/display_bounds.rst @@ -0,0 +1,231 @@ +.. _display_bounds: + +Testbounds +========== + +This is a working example that shows how you can use :struct:`Bounds` +to see the information about a part or vessel's shape. + +To run this example, simply copy and paste the file below into +a file in your archive folder called ``display_bounds.ks``. Then +put any vessel that has a kOS part on it on the launchpad, +and just run "0:/display_bounds.ks". The whole example is contained +in this one script so you won't need any other libraries. + +The best test vessels to try this on will be vessels that have a +variety of different types of parts. + +Press "N" for next part or "P" for previous part, and it will go +through the whole vessel showing you the bounds box for each part. +(At the start it's showing the bounds box for the whole vessel, +which you may get back to by using "P" for "Previous" part, all +the way to the start again.) + +To help show what kind of information a BOUNDS can give you, +this program prints a lot of the BOUNDS suffixes on the screen +as you iterate over the parts. + +This is not so much a "tutorial" as an "example" that shows +a lot of different kOS topics, such as: + +- Using VecDraw() to draw lines and arrows. +- Using Vector rotations by multiplying a bounds' ``:FACING`` + by a position vector, which converts that position vector from + a part-relative orientation to the absolute ship-raw reference + frame the rest of kOS uses. +- Using Delegates in the VecDraw() constructor to tell it how + to continually update its START and VEC properties. +- Using ``TERMINAL:INPUT:GETCHAR()`` to read keyboard input in + the terminal. +- Using ``Vessel:BOUNDS`` and ``Part:BOUNDS`` to get bounding box + information about the ship. + + +:: + + @lazyglobal off. + // + // display_bounds.ks + // ----------------- + // + // A small example program that will show you how to + // read the BOUNDS information of Vessels and Parts, + // and will display the box as a set of Vecdraws. + // You may iterate over the parts with the N and P + // keys. (The "-1 th" part is to show the whole + // vessel's box). + // + + // Because this example uses lots of delegates + // in its VECDRAWs, it needs the time to keep running + // those delegates each update, or else it really + // bogs down: + if CONFIG:IPU < 500 { + set CONFIG:IPU to 500. + HUDTEXT("NOTICE: EXAMPLE SCRIPT INCREASED CONFIG:IPU TO " + CONFIG:IPU, + 20, 2, 22, magenta, true). + } + + // ======================================= + // These are some utility functions for this + // example program to help display things: + // ======================================= + + function vector_tostring_rounded { + // Same thing that vector:tostring() normally + // does, but with more rounding so the display + // doesn't get so big: + parameter vec. + + return "V(" + + round(vec:x,2) + ", " + + round(vec:y,2) + ", " + + round(vec:z,2) + ")". + } + + local arrows is LIST(). + function draw_abs_to_box { + // Draws the vectors from origin TO the 2 opposite corners of the box: + + parameter B. + + // Wipe any old arrow draws off the screen. + for arrow in arrows { set arrow:show to false. } + wait 0. + + arrows:CLEAR(). + arrows:ADD(Vecdraw( + {return V(0,0,0).}, {return B:ABSMIN.}, RGB(1,0,0.75), "ABSMIN", 1, true)). + arrows:ADD(Vecdraw( + {return V(0,0,0).}, {return B:ABSMAX.}, RGB(1,0,0.75), "ABSMAX", 1, true)). + } + + local edges is LIST(). + function draw_box { + // Draws a bounds box as a set of 12 non-pointy + // vecdraws along the box edges: + parameter B. + + // Wipe any old edge draws off the screen. + for edge in edges { set edge:show to false. } + wait 0. + + // These need to calculate using relative coords to find all the box edges: + local rel_x_size is B:RELMAX:X - B:RELMIN:X. + local rel_y_size is B:RELMAX:Y - B:RELMIN:Y. + local rel_z_size is B:RELMAX:Z - B:RELMIN:Z. + + edges:CLEAR(). + + // The 4 edges parallel to the relative X axis: + edges:ADD(Vecdraw( + {return B:ABSORIGIN + B:FACING * V(B:RELMIN:X, B:RELMIN:Y, B:RELMIN:Z).}, + {return B:FACING * V(rel_x_size, 0, 0).}, + RGBA(1,0,1,0.75), "", 1, true, 0.02, false, false)). + edges:ADD(Vecdraw( + {return B:ABSORIGIN + B:FACING * V(B:RELMIN:X, B:RELMIN:Y, B:RELMAX:Z).}, + {return B:FACING * V(rel_x_size, 0, 0).}, + RGBA(1,0,1,0.75), "", 1, true, 0.02, false, false)). + edges:ADD(Vecdraw( + {return B:ABSORIGIN + B:FACING * V(B:RELMIN:X, B:RELMAX:Y, B:RELMAX:Z).}, + {return B:FACING * V(rel_x_size, 0, 0).}, + RGBA(1,0,1,0.75), "", 1, true, 0.02, false, false)). + edges:ADD(Vecdraw( + {return B:ABSORIGIN + B:FACING * V(B:RELMIN:X, B:RELMAX:Y, B:RELMIN:Z).}, + {return B:FACING * V(rel_x_size, 0, 0).}, + RGBA(1,0,1,0.75), "", 1, true, 0.02, false, false)). + + // The 4 edges parallel to the relative Y axis: + edges:ADD(Vecdraw( + {return B:ABSORIGIN + B:FACING * V(B:RELMIN:X, B:RELMIN:Y, B:RELMIN:Z).}, + {return B:FACING * V(0, rel_y_size, 0).}, + RGBA(1,0,1,0.75), "", 1, true, 0.02, false, false)). + edges:ADD(Vecdraw( + {return B:ABSORIGIN + B:FACING * V(B:RELMIN:X, B:RELMIN:Y, B:RELMAX:Z).}, + {return B:FACING * V(0, rel_y_size, 0).}, + RGBA(1,0,1,0.75), "", 1, true, 0.02, false, false)). + edges:ADD(Vecdraw( + {return B:ABSORIGIN + B:FACING * V(B:RELMAX:X, B:RELMIN:Y, B:RELMAX:Z).}, + {return B:FACING * V(0, rel_y_size, 0).}, + RGBA(1,0,1,0.75), "", 1, true, 0.02, false, false)). + edges:ADD(Vecdraw( + {return B:ABSORIGIN + B:FACING * V(B:RELMAX:X, B:RELMIN:Y, B:RELMIN:Z).}, + {return B:FACING * V(0, rel_y_size, 0).}, + RGBA(1,0,1,0.75), "", 1, true, 0.02, false, false)). + + // The 4 edges parallel to the relative Z axis: + edges:ADD(Vecdraw( + {return B:ABSORIGIN + B:FACING * V(B:RELMIN:X, B:RELMIN:Y, B:RELMIN:Z).}, + {return B:FACING * V(0, 0, rel_z_size).}, + RGBA(1,0,1,0.75), "", 1, true, 0.02, false, false)). + edges:ADD(Vecdraw( + {return B:ABSORIGIN + B:FACING * V(B:RELMIN:X, B:RELMAX:Y, B:RELMIN:Z).}, + {return B:FACING * V(0, 0, rel_z_size).}, + RGBA(1,0,1,0.75), "", 1, true, 0.02, false, false)). + edges:ADD(Vecdraw( + {return B:ABSORIGIN + B:FACING * V(B:RELMAX:X, B:RELMAX:Y, B:RELMIN:Z).}, + {return B:FACING * V(0, 0, rel_z_size).}, + RGBA(1,0,1,0.75), "", 1, true, 0.02, false, false)). + edges:ADD(Vecdraw( + {return B:ABSORIGIN + B:FACING * V(B:RELMAX:X, B:RELMIN:Y, B:RELMIN:Z).}, + {return B:FACING * V(0, 0, rel_z_size).}, + RGBA(1,0,1,0.75), "", 1, true, 0.02, false, false)). + } + + // + // =============================== + // main program + // =============================== + // + + local pNum is -1. + local keyPress is "". + + until keyPress = "q" { + + local box is 0. // will get set to the bounds box in a moment. + local description is "". + + clearscreen. + + if pNum = -1 { + // PART NUMBER -1 will be a special flag this + // example program uses to mean "entire vessel". + set box to ship:bounds. + set description to ship:TOSTRING(). + } else { + local p is ship:parts[pNum]. + set box to p:bounds. + set description to "Part[" + pNum + "]:" + p:TOSTRING(). + } + + // These two functions do the actual drawing, and are defined + // below in this file. When trying to learn how this works, + // look at draw_abs_to_box() first - it's the simpler one to + // understand that just uses absolute coordinates. The other + // one, draw_box(), is more complex as it has to use the + // relative coords to get all the other corners of the box: + draw_abs_to_box(box). + draw_box(box). + + print "Showing bounds of: " + description. + print "-----------------------------------------------------------". + print " ABSMIN: " + vector_tostring_rounded(box:ABSMIN). + print " ABSMAX: " + vector_tostring_rounded(box:ABSMAX). + print " ABSORIGIN: " + vector_tostring_rounded(box:ABSORIGIN). + print " ABSCENTER: " + vector_tostring_rounded(box:ABSCENTER). + print " RELMIN: " + vector_tostring_rounded(box:RELMIN). + print " RELMAX: " + vector_tostring_rounded(box:RELMAX). + print " EXTENTS: " + vector_tostring_rounded(box:EXTENTS). + print " SIZE: " + vector_tostring_rounded(box:SIZE). + print " RELCENTER: " + vector_tostring_rounded(box:RELCENTER). + print " BOTTOMALT: " + round(box:BOTTOMALT,2). + print "BOTTOMALTRADAR: " + round(box:BOTTOMALTRADAR,2). + print "-----------------------------------------------------------". + print "Press N for next, P for previous, Q for quit.". + set keyPress to terminal:input:getchar(). + if keyPress = "n" set pNum to min(ship:parts:length-1, pNum + 1). + if keyPress = "p" set pNum to max(-1, pNum - 1). + + clearvecdraws(). + } diff --git a/kerboscript_tests/lex_suffix_test1.ks b/kerboscript_tests/lex_suffix_test1.ks new file mode 100644 index 000000000..035e57ab1 --- /dev/null +++ b/kerboscript_tests/lex_suffix_test1.ks @@ -0,0 +1,37 @@ +local my_lex is lexicon( + "Key0" , "A", + "Key1" , "B", + "Key2" , "C", + "Key3" , "D", + "Key4" , "E", + "Key5" , "F"). + +print "Expect: ABCDEF". +print "Actual: " + concat_lex(my_lex). + +set my_lex:key1 to "_". +set my_lex:key3 to "_". +set my_lex:key5 to "_". + +print "Expect: A_C_E_". +print "Actual: " + concat_lex(my_lex). + +set my_lex:key1 to { return "%". }. +set my_lex:key3 to { local a is 1. local b is 4. return a+b. }. +set my_lex:key5 to { return 3/2. }. + +print "Expect: A%C5E1.5". +print "Actual: " + concat_lex(my_lex). + +function concat_lex { + parameter the_lex. + + local str is "". + for key in the_lex:keys { + if the_lex[key]:istype("Delegate") + set str to str + the_lex[key](). + else + set str to str + the_lex[key]. + } + return str. +} diff --git a/kerboscript_tests/lex_suffix_test2.ks b/kerboscript_tests/lex_suffix_test2.ks new file mode 100644 index 000000000..bc3b99df2 --- /dev/null +++ b/kerboscript_tests/lex_suffix_test2.ks @@ -0,0 +1,39 @@ +print "Testing using Lex as a psuedo-class.". +print "------------------------------------". + + +print "Making 'fred', an instance of person'.". +local fred is construct_person("Fred", 23). +print "fred:greet() prints this:". +print fred:greet(). + +print "Making 'henri', an instance of frenchperson.". +local henri is construct_frenchperson("Henri", 19). +print henri:greet(). + +function construct_person { + parameter name, age. + + local myself is LEX(). + + set myself:name to name. + set myself:age to age. + set myself:greet to { + return "Hello, my name is " + myself:name +" and I am " + myself:age + " years old.". + }. + + return myself. +}. + +function construct_frenchperson { + parameter name, age. + + local myself is construct_person(name, age). + + // This is sort of like overriding a method: + set myself:greet to { + return "Bonjour, Je m'appelle " + myself:name + " et j'ai " + myself:age + " ans.". + }. + + return myself. +} diff --git a/kerboscript_tests/lex_suffix_test3.ks b/kerboscript_tests/lex_suffix_test3.ks new file mode 100644 index 000000000..b68893677 --- /dev/null +++ b/kerboscript_tests/lex_suffix_test3.ks @@ -0,0 +1,42 @@ +local empty_lex is lexicon(). +local populated_lex is lexicon( + "key0", 0, // valid suffix name + "key1", 10, // valid suffix name + "key2", 20, // valid suffix name + "KEY3", 30, // valid suffix name + // None of the following 3 should be valid suffix names because of the + // spaces: + " SpaceBefore", "----", + "SpaceAfter ", "----", + "Space Between", "----", + V(1,0,0), "----", // not a valid suffix name because Vectors aren't strings. + "zzzzzz", 9999 // valid suffix name + ). +print "Expect: True". +print "Actual: " + empty_lex:hassuffix("add"). // built-in suffix for all lex's. +print " ". +print "Expect: True". +print "Actual: " + populated_lex:hassuffix("add"). // built-in suffix for all lex's. +print " ". +print "Expect: False". +print "Actual: " + empty_lex:hassuffix("key2"). // only exists if key2 is in the lex. +print " ". +print "Expect: True". +print "Actual: " + populated_lex:hassuffix("key2"). // only exists if key2 is in the lex. +print " ". +print "Expect: True". +print "Actual: " + populated_lex:hassuffix("kEy3"). // case insensitive check. +print " ". +print "Expect: True". +print "Actual: " + populated_lex:haskey("Space Between"). // key exists +print " ". +print "Expect: False". +print "Actual: " + populated_lex:hassuffix("Space Between"). // but isn't a valid suffix name + +// There should be 5 more suffixes in the populated list than in default lex's, +// because 5 of the keys in it form valid identifier strings: +// If this is more than 5, then keys that shouldn't be +// suffixes are getting into the list. +print "Expect: 5". +print "Actual: " + (populated_lex:suffixnames:length - empty_lex:suffixnames:length). + diff --git a/kerboscript_tests/ternary.ks b/kerboscript_tests/ternary.ks new file mode 100644 index 000000000..e15b207f8 --- /dev/null +++ b/kerboscript_tests/ternary.ks @@ -0,0 +1,38 @@ +print "THESE are tests of the ternary CHOOSE operator.". + +Print "Basic case - Next line should print 'A':". +print choose "A" if true else "B". + +Print "Basic case - Next line should print 'B':". +print choose "A" if false else "B". + + +print "Testing nested CHOOSEs. Next line should be 'ABCDE*****':". +set str to "". +for i in range(0,10) { + set str to str + (choose "A" if i = 0 else choose "B" if i = 1 else choose "C" if i = 2 else choose "D" if i = 3 else choose "E" if i = 4 else "*"). + +} +print str. + + +print "Weird case - nesting choose in the boolean.". +set x to true. +set y to false. +set a to 1. +print " Next 2 lines should be 'A':". +print " " + (choose "A" if (choose x if a=1 else y) else "B"). +print " " + (choose "A" if choose x if a=1 else y else "B"). // same without parens should work. +print " Next 2 lines should be 'B':". +print " " + (choose "A" if (choose x if a=2 else y) else "B"). +print " " + (choose "A" if choose x if a=2 else y else "B"). // same without parens should work. + +print "Complex case - selecting delegate with choose.". +set a to 1. +set del1 to choose { print "trueDel". } if a = 1 else { print "falseDel.". }. +set a to 2. +set del2 to choose { print "trueDel". } if a = 1 else { print "falseDel.". }. +print "Next line should say 'trueDel':". +del1:call(). +print "Next line should say 'falseDel':". +del2:call(). diff --git a/src/kOS.Safe.Test/Execution/Config.cs b/src/kOS.Safe.Test/Execution/Config.cs index 39c57a3f4..ed7bace71 100644 --- a/src/kOS.Safe.Test/Execution/Config.cs +++ b/src/kOS.Safe.Test/Execution/Config.cs @@ -1,4 +1,4 @@ -using kOS.Safe.Encapsulation; +using kOS.Safe.Encapsulation; using kOS.Safe.Encapsulation.Suffixes; using System; using System.Collections.Generic; @@ -163,6 +163,30 @@ public string TerminalFontName } } + public int TerminalDefaultWidth + { + get + { + return 0; + } + + set + { + } + } + + public int TerminalDefaultHeight + { + get + { + return 0; + } + + set + { + } + } + public DateTime TimeStamp { get @@ -212,7 +236,7 @@ public IList GetConfigKeys() return new List(); } - public ISuffixResult GetSuffix(string suffixName) + public ISuffixResult GetSuffix(string suffixName, bool failOkay = false) { throw new NotImplementedException(); } @@ -222,7 +246,7 @@ public void SaveConfig() throw new NotImplementedException(); } - public bool SetSuffix(string suffixName, object value) + public bool SetSuffix(string suffixName, object value, bool failOkay = false) { throw new NotImplementedException(); } diff --git a/src/kOS.Safe.Test/Opcode/FakeCpu.cs b/src/kOS.Safe.Test/Opcode/FakeCpu.cs index 772809527..3be5ffd25 100644 --- a/src/kOS.Safe.Test/Opcode/FakeCpu.cs +++ b/src/kOS.Safe.Test/Opcode/FakeCpu.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using kOS.Safe.Execution; using kOS.Safe.Encapsulation; @@ -52,6 +52,11 @@ public void PushNewScope(Int16 scopeId, Int16 parentScopeId) throw new NotImplementedException(); } + public void DropBackPriority() + { + throw new NotImplementedException(); + } + public List GetCurrentClosure() { throw new NotImplementedException(); diff --git a/src/kOS.Safe.Test/Properties/AssemblyInfo.cs b/src/kOS.Safe.Test/Properties/AssemblyInfo.cs index efc9e9b29..60c3d52a8 100644 --- a/src/kOS.Safe.Test/Properties/AssemblyInfo.cs +++ b/src/kOS.Safe.Test/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyFileVersion("1.1.8.0")] -[assembly: AssemblyVersion("1.1.8.0")] +[assembly: AssemblyFileVersion("1.1.9.0")] +[assembly: AssemblyVersion("1.1.9.0")] diff --git a/src/kOS.Safe/Compilation/KS/Compiler.cs b/src/kOS.Safe/Compilation/KS/Compiler.cs index 298e0d958..1d7094273 100644 --- a/src/kOS.Safe/Compilation/KS/Compiler.cs +++ b/src/kOS.Safe/Compilation/KS/Compiler.cs @@ -282,6 +282,7 @@ private void IterateUserFunctions(ParseNode node, Action action) case TokenType.COMPARATOR: case TokenType.AND: case TokenType.OR: + case TokenType.CHOOSE: case TokenType.directive: doChildren = false; break; @@ -1149,6 +1150,9 @@ private void VisitNode(ParseNode node) case TokenType.expr: VisitExpr(node); break; + case TokenType.ternary_expr: + VisitTernary(node); + break; case TokenType.or_expr: case TokenType.and_expr: VisitShortCircuitBoolean(node); @@ -1352,11 +1356,38 @@ private void VisitExpr(ParseNode node) } } + private void VisitTernary(ParseNode node) + { + NodeStartHousekeeping(node); + + // Syntax pattern is: + // [0] = keyword CHOOSE + // [1] = expression returned if true + // [2] = keyword IF + // [3] = expression with boolean value + // [4] = keyword ELSE + // [5] = expression returned if false + + VisitNode(node.Nodes[3]); // eval the boolean clause, put on stack. + + Opcode bypassTrue = AddOpcode(new OpcodeBranchIfFalse()); + + VisitNode(node.Nodes[1]); // expression if true. + Opcode bypassFalse = AddOpcode(new OpcodeBranchJump()); + + bypassTrue.DestinationLabel = GetNextLabel(false); + + VisitNode(node.Nodes[5]); // expression if false. + + bypassFalse.DestinationLabel = GetNextLabel(false); + addBranchDestination = true; + } + /// /// Handles the short-circuit logic of boolean OR and boolean AND /// chains. It is like VisitExpressionChain (see elsewhere) but /// in this case it has the special logic to short circuit and skip - /// executing the righthand expression if it can. (The generic VisitExpressionXhain + /// executing the righthand expression if it can. (The generic VisitExpressionChain /// always evaluates both the left and right sides of the operator first, then /// does the operation). /// @@ -2509,7 +2540,7 @@ private void VisitLockStatement(ParseNode node, StorageModifier whereToStore) { Trigger triggerObject = context.Triggers.GetTrigger(triggerIdentifier); AddOpcode(new OpcodePushRelocateLater(null), triggerObject.GetFunctionLabel()); - AddOpcode(new OpcodeAddTrigger(false)); + AddOpcode(new OpcodeAddTrigger(false, InterruptPriority.RecurringControl)); } // enable this FlyByWire parameter @@ -2590,7 +2621,7 @@ private void VisitOnStatement(ParseNode node) VisitNode(node.Nodes[1]); // the expression in the on statement. AddOpcode(new OpcodeStore(triggerObject.OldValueIdentifier)); AddOpcode(new OpcodePushRelocateLater(null), triggerObject.GetFunctionLabel()); - AddOpcode(new OpcodeAddTrigger()); + AddOpcode(new OpcodeAddTrigger(InterruptPriority.Recurring)); } } @@ -2604,7 +2635,7 @@ private void VisitWhenStatement(ParseNode node) if (triggerObject.IsInitialized()) { AddOpcode(new OpcodePushRelocateLater(null), triggerObject.GetFunctionLabel()); - AddOpcode(new OpcodeAddTrigger()); + AddOpcode(new OpcodeAddTrigger(InterruptPriority.Recurring)); } } diff --git a/src/kOS.Safe/Compilation/KS/KSScript.cs b/src/kOS.Safe/Compilation/KS/KSScript.cs index 4fee5b92b..e719484fb 100644 --- a/src/kOS.Safe/Compilation/KS/KSScript.cs +++ b/src/kOS.Safe/Compilation/KS/KSScript.cs @@ -1,5 +1,6 @@ using kOS.Safe.Exceptions; using System.Collections.Generic; +using System.Text.RegularExpressions; using kOS.Safe.Persistence; namespace kOS.Safe.Compilation.KS diff --git a/src/kOS.Safe/Compilation/KS/ParseTree.cs b/src/kOS.Safe/Compilation/KS/ParseTree.cs index 31c312b37..dce66ca99 100644 --- a/src/kOS.Safe/Compilation/KS/ParseTree.cs +++ b/src/kOS.Safe/Compilation/KS/ParseTree.cs @@ -319,6 +319,9 @@ internal object Eval(ParseTree tree, params object[] paramlist) case TokenType.expr: Value = Evalexpr(tree, paramlist); break; + case TokenType.ternary_expr: + Value = Evalternary_expr(tree, paramlist); + break; case TokenType.or_expr: Value = Evalor_expr(tree, paramlist); break; @@ -711,6 +714,13 @@ protected virtual object Evalexpr(ParseTree tree, params object[] paramlist) return null; } + protected virtual object Evalternary_expr(ParseTree tree, params object[] paramlist) + { + foreach (var node in Nodes) + node.Eval(tree, paramlist); + return null; + } + protected virtual object Evalor_expr(ParseTree tree, params object[] paramlist) { foreach (var node in Nodes) diff --git a/src/kOS.Safe/Compilation/KS/Parser.cs b/src/kOS.Safe/Compilation/KS/Parser.cs index ad6894a41..2dd1edfb9 100644 --- a/src/kOS.Safe/Compilation/KS/Parser.cs +++ b/src/kOS.Safe/Compilation/KS/Parser.cs @@ -1749,8 +1749,9 @@ private void Parsereturn_stmt(ParseNode parent) // NonTerminalSymbol: return_stm } // Concat Rule - tok = scanner.LookAhead(TokenType.PLUSMINUS, TokenType.NOT, TokenType.DEFINED, TokenType.INTEGER, TokenType.DOUBLE, TokenType.TRUEFALSE, TokenType.IDENTIFIER, TokenType.FILEIDENT, TokenType.BRACKETOPEN, TokenType.STRING, TokenType.CURLYOPEN); // Option Rule - if (tok.Type == TokenType.PLUSMINUS + tok = scanner.LookAhead(TokenType.CHOOSE, TokenType.PLUSMINUS, TokenType.NOT, TokenType.DEFINED, TokenType.INTEGER, TokenType.DOUBLE, TokenType.TRUEFALSE, TokenType.IDENTIFIER, TokenType.FILEIDENT, TokenType.BRACKETOPEN, TokenType.STRING, TokenType.CURLYOPEN); // Option Rule + if (tok.Type == TokenType.CHOOSE + || tok.Type == TokenType.PLUSMINUS || tok.Type == TokenType.NOT || tok.Type == TokenType.DEFINED || tok.Type == TokenType.INTEGER @@ -2674,9 +2675,12 @@ private void Parseexpr(ParseNode parent) // NonTerminalSymbol: expr ParseNode node = parent.CreateNode(scanner.GetToken(TokenType.expr), "expr"); parent.Nodes.Add(node); - tok = scanner.LookAhead(TokenType.PLUSMINUS, TokenType.NOT, TokenType.DEFINED, TokenType.INTEGER, TokenType.DOUBLE, TokenType.TRUEFALSE, TokenType.IDENTIFIER, TokenType.FILEIDENT, TokenType.BRACKETOPEN, TokenType.STRING, TokenType.CURLYOPEN); // Choice Rule + tok = scanner.LookAhead(TokenType.CHOOSE, TokenType.PLUSMINUS, TokenType.NOT, TokenType.DEFINED, TokenType.INTEGER, TokenType.DOUBLE, TokenType.TRUEFALSE, TokenType.IDENTIFIER, TokenType.FILEIDENT, TokenType.BRACKETOPEN, TokenType.STRING, TokenType.CURLYOPEN); // Choice Rule switch (tok.Type) { // Choice Rule + case TokenType.CHOOSE: + Parseternary_expr(node); // NonTerminal Rule: ternary_expr + break; case TokenType.PLUSMINUS: case TokenType.NOT: case TokenType.DEFINED: @@ -2693,13 +2697,63 @@ private void Parseexpr(ParseNode parent) // NonTerminalSymbol: expr Parseinstruction_block(node); // NonTerminal Rule: instruction_block break; default: - tree.Errors.Add(new ParseError("Unexpected token '" + tok.Text.Replace("\n", "") + "' found. Expected PLUSMINUS, NOT, DEFINED, INTEGER, DOUBLE, TRUEFALSE, IDENTIFIER, FILEIDENT, BRACKETOPEN, STRING, or CURLYOPEN.", 0x0002, tok)); + tree.Errors.Add(new ParseError("Unexpected token '" + tok.Text.Replace("\n", "") + "' found. Expected CHOOSE, PLUSMINUS, NOT, DEFINED, INTEGER, DOUBLE, TRUEFALSE, IDENTIFIER, FILEIDENT, BRACKETOPEN, STRING, or CURLYOPEN.", 0x0002, tok)); break; } // Choice Rule parent.Token.UpdateRange(node.Token); } // NonTerminalSymbol: expr + private void Parseternary_expr(ParseNode parent) // NonTerminalSymbol: ternary_expr + { + Token tok; + ParseNode n; + ParseNode node = parent.CreateNode(scanner.GetToken(TokenType.ternary_expr), "ternary_expr"); + parent.Nodes.Add(node); + + + // Concat Rule + tok = scanner.Scan(TokenType.CHOOSE); // Terminal Rule: CHOOSE + n = node.CreateNode(tok, tok.ToString() ); + node.Token.UpdateRange(tok); + node.Nodes.Add(n); + if (tok.Type != TokenType.CHOOSE) { + tree.Errors.Add(new ParseError("Unexpected token '" + tok.Text.Replace("\n", "") + "' found. Expected " + TokenType.CHOOSE.ToString(), 0x1001, tok)); + return; + } + + // Concat Rule + Parseexpr(node); // NonTerminal Rule: expr + + // Concat Rule + tok = scanner.Scan(TokenType.IF); // Terminal Rule: IF + n = node.CreateNode(tok, tok.ToString() ); + node.Token.UpdateRange(tok); + node.Nodes.Add(n); + if (tok.Type != TokenType.IF) { + tree.Errors.Add(new ParseError("Unexpected token '" + tok.Text.Replace("\n", "") + "' found. Expected " + TokenType.IF.ToString(), 0x1001, tok)); + return; + } + + // Concat Rule + Parseexpr(node); // NonTerminal Rule: expr + + // Concat Rule + tok = scanner.Scan(TokenType.ELSE); // Terminal Rule: ELSE + n = node.CreateNode(tok, tok.ToString() ); + node.Token.UpdateRange(tok); + node.Nodes.Add(n); + if (tok.Type != TokenType.ELSE) { + tree.Errors.Add(new ParseError("Unexpected token '" + tok.Text.Replace("\n", "") + "' found. Expected " + TokenType.ELSE.ToString(), 0x1001, tok)); + return; + } + + // Concat Rule + Parseexpr(node); // NonTerminal Rule: expr + + parent.Token.UpdateRange(node.Token); + } // NonTerminalSymbol: ternary_expr + private void Parseor_expr(ParseNode parent) // NonTerminalSymbol: or_expr { Token tok; @@ -3103,8 +3157,9 @@ private void Parsefunction_trailer(ParseNode parent) // NonTerminalSymbol: funct } // Concat Rule - tok = scanner.LookAhead(TokenType.PLUSMINUS, TokenType.NOT, TokenType.DEFINED, TokenType.INTEGER, TokenType.DOUBLE, TokenType.TRUEFALSE, TokenType.IDENTIFIER, TokenType.FILEIDENT, TokenType.BRACKETOPEN, TokenType.STRING, TokenType.CURLYOPEN); // Option Rule - if (tok.Type == TokenType.PLUSMINUS + tok = scanner.LookAhead(TokenType.CHOOSE, TokenType.PLUSMINUS, TokenType.NOT, TokenType.DEFINED, TokenType.INTEGER, TokenType.DOUBLE, TokenType.TRUEFALSE, TokenType.IDENTIFIER, TokenType.FILEIDENT, TokenType.BRACKETOPEN, TokenType.STRING, TokenType.CURLYOPEN); // Option Rule + if (tok.Type == TokenType.CHOOSE + || tok.Type == TokenType.PLUSMINUS || tok.Type == TokenType.NOT || tok.Type == TokenType.DEFINED || tok.Type == TokenType.INTEGER diff --git a/src/kOS.Safe/Compilation/KS/Scanner.cs b/src/kOS.Safe/Compilation/KS/Scanner.cs index 57f7dfaee..4b47fad08 100644 --- a/src/kOS.Safe/Compilation/KS/Scanner.cs +++ b/src/kOS.Safe/Compilation/KS/Scanner.cs @@ -280,6 +280,10 @@ public Scanner() Patterns.Add(TokenType.UNSET, regex); Tokens.Add(TokenType.UNSET); + regex = new Regex(@"\G(?:choose\b)"); + Patterns.Add(TokenType.CHOOSE, regex); + Tokens.Add(TokenType.CHOOSE); + regex = new Regex(@"\G(?:\()"); Patterns.Add(TokenType.BRACKETOPEN, regex); Tokens.Add(TokenType.BRACKETOPEN); @@ -576,109 +580,111 @@ public enum TokenType unset_stmt= 46, arglist = 47, expr = 48, - or_expr = 49, - and_expr= 50, - compare_expr= 51, - arith_expr= 52, - multdiv_expr= 53, - unary_expr= 54, - factor = 55, - suffix = 56, - suffix_trailer= 57, - suffixterm= 58, - suffixterm_trailer= 59, - function_trailer= 60, - array_trailer= 61, - atom = 62, - sci_number= 63, - number = 64, - varidentifier= 65, - identifier_led_stmt= 66, - identifier_led_expr= 67, + ternary_expr= 49, + or_expr = 50, + and_expr= 51, + compare_expr= 52, + arith_expr= 53, + multdiv_expr= 54, + unary_expr= 55, + factor = 56, + suffix = 57, + suffix_trailer= 58, + suffixterm= 59, + suffixterm_trailer= 60, + function_trailer= 61, + array_trailer= 62, + atom = 63, + sci_number= 64, + number = 65, + varidentifier= 66, + identifier_led_stmt= 67, + identifier_led_expr= 68, //Terminal tokens: - PLUSMINUS= 68, - MULT = 69, - DIV = 70, - POWER = 71, - E = 72, - NOT = 73, - AND = 74, - OR = 75, - TRUEFALSE= 76, - COMPARATOR= 77, - SET = 78, - TO = 79, - IS = 80, - IF = 81, - ELSE = 82, - UNTIL = 83, - STEP = 84, - DO = 85, - LOCK = 86, - UNLOCK = 87, - PRINT = 88, - AT = 89, - ON = 90, - TOGGLE = 91, - WAIT = 92, - WHEN = 93, - THEN = 94, - OFF = 95, - STAGE = 96, - CLEARSCREEN= 97, - ADD = 98, - REMOVE = 99, - LOG = 100, - BREAK = 101, - PRESERVE= 102, - DECLARE = 103, - DEFINED = 104, - LOCAL = 105, - GLOBAL = 106, - PARAMETER= 107, - FUNCTION= 108, - RETURN = 109, - SWITCH = 110, - COPY = 111, - FROM = 112, - RENAME = 113, - VOLUME = 114, - FILE = 115, - DELETE = 116, - EDIT = 117, - RUN = 118, - RUNPATH = 119, - RUNONCEPATH= 120, - ONCE = 121, - COMPILE = 122, - LIST = 123, - REBOOT = 124, - SHUTDOWN= 125, - FOR = 126, - UNSET = 127, - BRACKETOPEN= 128, - BRACKETCLOSE= 129, - CURLYOPEN= 130, - CURLYCLOSE= 131, - SQUAREOPEN= 132, - SQUARECLOSE= 133, - COMMA = 134, - COLON = 135, - IN = 136, - ARRAYINDEX= 137, - ALL = 138, - IDENTIFIER= 139, - FILEIDENT= 140, - INTEGER = 141, - DOUBLE = 142, - STRING = 143, - EOI = 144, - ATSIGN = 145, - LAZYGLOBAL= 146, - EOF = 147, - WHITESPACE= 148, - COMMENTLINE= 149 + PLUSMINUS= 69, + MULT = 70, + DIV = 71, + POWER = 72, + E = 73, + NOT = 74, + AND = 75, + OR = 76, + TRUEFALSE= 77, + COMPARATOR= 78, + SET = 79, + TO = 80, + IS = 81, + IF = 82, + ELSE = 83, + UNTIL = 84, + STEP = 85, + DO = 86, + LOCK = 87, + UNLOCK = 88, + PRINT = 89, + AT = 90, + ON = 91, + TOGGLE = 92, + WAIT = 93, + WHEN = 94, + THEN = 95, + OFF = 96, + STAGE = 97, + CLEARSCREEN= 98, + ADD = 99, + REMOVE = 100, + LOG = 101, + BREAK = 102, + PRESERVE= 103, + DECLARE = 104, + DEFINED = 105, + LOCAL = 106, + GLOBAL = 107, + PARAMETER= 108, + FUNCTION= 109, + RETURN = 110, + SWITCH = 111, + COPY = 112, + FROM = 113, + RENAME = 114, + VOLUME = 115, + FILE = 116, + DELETE = 117, + EDIT = 118, + RUN = 119, + RUNPATH = 120, + RUNONCEPATH= 121, + ONCE = 122, + COMPILE = 123, + LIST = 124, + REBOOT = 125, + SHUTDOWN= 126, + FOR = 127, + UNSET = 128, + CHOOSE = 129, + BRACKETOPEN= 130, + BRACKETCLOSE= 131, + CURLYOPEN= 132, + CURLYCLOSE= 133, + SQUAREOPEN= 134, + SQUARECLOSE= 135, + COMMA = 136, + COLON = 137, + IN = 138, + ARRAYINDEX= 139, + ALL = 140, + IDENTIFIER= 141, + FILEIDENT= 142, + INTEGER = 143, + DOUBLE = 144, + STRING = 145, + EOI = 146, + ATSIGN = 147, + LAZYGLOBAL= 148, + EOF = 149, + WHITESPACE= 150, + COMMENTLINE= 151 } public class Token diff --git a/src/kOS.Safe/Compilation/KS/kRISC.tpg b/src/kOS.Safe/Compilation/KS/kRISC.tpg index 654531f48..c3c6d098f 100644 --- a/src/kOS.Safe/Compilation/KS/kRISC.tpg +++ b/src/kOS.Safe/Compilation/KS/kRISC.tpg @@ -65,6 +65,7 @@ REBOOT -> @"reboot\b"; SHUTDOWN -> @"shutdown\b"; FOR -> @"for\b"; UNSET -> @"unset\b"; +CHOOSE -> @"choose\b"; //Generic BRACKETOPEN -> @"\("; @@ -78,7 +79,12 @@ COLON -> @":"; IN -> @"in\b"; ARRAYINDEX -> @"#"; ALL -> @"all\b"; -IDENTIFIER -> @"[_\p{L}]\w*"; + +// WARNING - IF YOU EDIT THE REGEX FOR IDENTIFIER ON THE NEXT LINE, +// THEN ALSO EDIT kOS.Safe.Utilities.StringUtil.IsValidIdentifier() +// TO USE THE SAME REGEX !!!!! +IDENTIFIER -> @"[_\p{L}]\w*"; //<---- Important - see above Comment!!!!! + FILEIDENT -> @"[_\p{L}]\w*(\.[_\p{L}]\w*)*"; INTEGER -> @"\d[_\d]*"; DOUBLE -> @"(\d+(?:_\d*)*)?\.\d+(?:_\d*)*"; @@ -207,7 +213,8 @@ unset_stmt -> UNSET (IDENTIFIER | ALL) EOI; // ---------- expressions --------------------------- arglist -> expr (COMMA expr)*; -expr -> (or_expr|instruction_block); +expr -> (ternary_expr|or_expr|instruction_block); +ternary_expr -> CHOOSE expr IF expr ELSE expr; or_expr -> and_expr (OR and_expr)*; and_expr -> compare_expr (AND compare_expr)*; compare_expr -> arith_expr (COMPARATOR arith_expr)*; diff --git a/src/kOS.Safe/Compilation/Opcode.cs b/src/kOS.Safe/Compilation/Opcode.cs index a79fb403e..cdbc94dbe 100644 --- a/src/kOS.Safe/Compilation/Opcode.cs +++ b/src/kOS.Safe/Compilation/Opcode.cs @@ -2196,7 +2196,7 @@ public override void Execute(ICpu cpu) cpu.PushArgumentStack(new BooleanValue((sr == null ? false : sr.IsCancelled))); } } - + /// /// /// Push the thing atop the stack onto the stack again so there are now two of it atop the stack. @@ -2471,7 +2471,7 @@ public OpcodePushDelegateRelocateLater(string destLabel, bool withClosure) : bas /// protected OpcodePushDelegateRelocateLater() {} - + public override void PopulateFromMLFields(List fields) { // Expect fields in the same order as the [MLField] properties of this class: @@ -2489,14 +2489,17 @@ public override void PopulateFromMLFields(List fields) /// /// /// Pops a function pointer from the stack and adds a trigger that will be called each cycle. - /// These triggers get the priority InterruptPriority.Recurring + /// The argument (to the opcode, not on the stack) contains the Interrupt Priority level + /// of the trigger. For one trigger to interrupt another, it needs a higher priority, + /// else it waits until the first trigger is completed before it will fire. /// /// - /// addtrigger + /// addtrigger N /// ... fp -- ... /// public class OpcodeAddTrigger : Opcode { + protected override string Name { get { return "addtrigger"; } } public override ByteCode Code { get { return ByteCode.ADDTRIGGER; } } @@ -2505,17 +2508,42 @@ public class OpcodeAddTrigger : Opcode /// that identifies this instance/entrypoint uniquely at runtime. /// (For example, ON triggers need this, but WHEN triggers do not). /// - [MLField(1,true)] + [MLField(1,false)] public bool Unique { get; set; } + /// + /// The interrupt priority level of the trigger. + /// (It's an Int32 type instead of InterruptPrioirity purely because MLFields + /// need to be one of the limited primitive types the system knows how to store.) + /// + [MLField(2, false)] + public Int32 Priority { get; set; } - public OpcodeAddTrigger(bool unique) + public OpcodeAddTrigger(bool unique, InterruptPriority priority) { Unique = unique; + Priority = (Int32)priority; } - public OpcodeAddTrigger() // Must have a defualt constructor for how KSM files work. + public OpcodeAddTrigger(InterruptPriority priority) // Must have a defualt constructor for how KSM files work. { Unique = true; + Priority = (Int32)priority; + } + + /// Only here because the compile storage system requires a default constructor. + /// It's private because we want to force everyone ELSE to use one of the versions with args. + /// + private OpcodeAddTrigger() + { + } + + public override void PopulateFromMLFields(List fields) + { + // Expect fields in the same order as the [MLField] properties of this class: + if (fields == null || fields.Count < 2) + throw new Exception("Saved field in ML file for OpcodeAddTrigger seems to be missing. Version mismatch?"); + Unique = Convert.ToBoolean(fields[0]); + Priority = Convert.ToInt32(fields[1]); } public override void Execute(ICpu cpu) @@ -2523,12 +2551,12 @@ public override void Execute(ICpu cpu) int functionPointer = Convert.ToInt32(cpu.PopValueArgument()); // in case it got wrapped in a ScalarIntValue List args = new List(); - cpu.AddTrigger(functionPointer, InterruptPriority.Recurring, (Unique ? cpu.NextTriggerInstanceId : 0), false, cpu.GetCurrentClosure()); + cpu.AddTrigger(functionPointer, (InterruptPriority) Priority, (Unique ? cpu.NextTriggerInstanceId : 0), false, cpu.GetCurrentClosure()); } public override string ToString() { - return Name; + return string.Format("{0}{1}, Pri {2} ", Name, (Unique ? " unique" : ""), Priority ); } } diff --git a/src/kOS.Safe/Encapsulation/ConstantValue.cs b/src/kOS.Safe/Encapsulation/ConstantValue.cs index 20dee765a..da762e691 100644 --- a/src/kOS.Safe/Encapsulation/ConstantValue.cs +++ b/src/kOS.Safe/Encapsulation/ConstantValue.cs @@ -26,7 +26,46 @@ public static double GravConst get { return gravConstBeingUsed; } set { gravConstBeingUsed = value; } } - + + private static double avogadroFromWIKI = 6.02214076 * Math.Pow(10, 23); + private static double avogadroBeingUsed = avogadroFromWIKI; + + /// + /// This is a public property so that it can be overridden by KSP-aware code elsewhere: + /// (ConstantValue is in kOS.Safe, so we can't see KSP's G value from here.) + /// + public static double AvogadroConst + { + get { return avogadroBeingUsed; } + set { avogadroBeingUsed = value; } + } + + private static double boltzmannFromWiki = 1.380649 * Math.Pow(10, -23); + private static double boltzmannBeingUsed = boltzmannFromWiki; + + /// + /// This is a public property so that it can be overridden by KSP-aware code elsewhere: + /// (ConstantValue is in kOS.Safe, so we can't see KSP's G value from here.) + /// + public static double BoltzmannConst + { + get { return boltzmannBeingUsed; } + set { boltzmannBeingUsed = value; } + } + + private static double idealGasFromWiki = 8.31446215324; + private static double idealGasBeingUsed = idealGasFromWiki; + + /// + /// This is a public property so that it can be overridden by KSP-aware code elsewhere: + /// (ConstantValue is in kOS.Safe, so we can't see KSP's G value from here.) + /// + public static double IdealGasConst + { + get { return idealGasBeingUsed; } + set { idealGasBeingUsed = value; } + } + private static double g0 = 9.80665; // Typically accepted Earth value. Will override with KSP game value. public static double G0 { @@ -49,6 +88,10 @@ static ConstantValue() // 180/pi : AddGlobalSuffix("RADTODEG", new StaticSuffix(() => 57.295779513082320876798154814105, "radians to degrees")); + + AddGlobalSuffix("AVOGADRO", new StaticSuffix(() => AvogadroConst)); + AddGlobalSuffix("BOLTZMANN", new StaticSuffix(() => BoltzmannConst)); + AddGlobalSuffix("IDEALGAS", new StaticSuffix(() => IdealGasConst)); } public override string ToString() diff --git a/src/kOS.Safe/Encapsulation/IConfig.cs b/src/kOS.Safe/Encapsulation/IConfig.cs index 99d321b58..cbf4ee381 100644 --- a/src/kOS.Safe/Encapsulation/IConfig.cs +++ b/src/kOS.Safe/Encapsulation/IConfig.cs @@ -21,6 +21,8 @@ public interface IConfig: ISuffixed int TerminalFontDefaultSize {get; set; } string TerminalFontName {get; set; } double TerminalBrightness {get; set; } + int TerminalDefaultWidth { get; set; } + int TerminalDefaultHeight { get; set; } /// diff --git a/src/kOS.Safe/Encapsulation/ISuffixed.cs b/src/kOS.Safe/Encapsulation/ISuffixed.cs index 1f781550a..0cc669b82 100644 --- a/src/kOS.Safe/Encapsulation/ISuffixed.cs +++ b/src/kOS.Safe/Encapsulation/ISuffixed.cs @@ -1,10 +1,10 @@ -using kOS.Safe.Encapsulation.Suffixes; +using kOS.Safe.Encapsulation.Suffixes; namespace kOS.Safe.Encapsulation { public interface ISuffixed { - bool SetSuffix(string suffixName, object value); - ISuffixResult GetSuffix(string suffixName); + bool SetSuffix(string suffixName, object value, bool failOkay = false); + ISuffixResult GetSuffix(string suffixName, bool failOkay = false); } } \ No newline at end of file diff --git a/src/kOS.Safe/Encapsulation/Lexicon.cs b/src/kOS.Safe/Encapsulation/Lexicon.cs index 623a30857..e62128105 100644 --- a/src/kOS.Safe/Encapsulation/Lexicon.cs +++ b/src/kOS.Safe/Encapsulation/Lexicon.cs @@ -63,11 +63,13 @@ public int GetHashCode(TI obj) } private IDictionary internalDictionary; + private IDictionary> keySuffixes; private bool caseSensitive; public Lexicon() { internalDictionary = new Dictionary(new LexiconComparer()); + keySuffixes = new Dictionary>(new LexiconComparer()); caseSensitive = false; InitalizeSuffixes(); } @@ -137,6 +139,13 @@ private void SetCaseSensitivity(BooleanValue value) internalDictionary = newCase ? new Dictionary() : new Dictionary(new LexiconComparer()); + + // Regardless of whether or not the lexicon itself is case sensitive, + // the key Suffixes have to be IN-sensitive because they are getting + // values who's case got squashed by the compiler. This needs to + // be documented well in the user docs (i.e. using the suffix syntax + // cannot detect the difference between keys that differ only in case). + keySuffixes = new Dictionary>(new LexiconComparer()); } private BooleanValue HasValue(Structure value) @@ -293,6 +302,98 @@ public override string ToString() return new SafeSerializationMgr(null).ToString(this); } + // Try to call the normal SetSuffix that all structures do, but if that fails, + // then try to use this suffix name as a key and set the value in the lexicon + // at that key. This can insert new key values in the lexicon, just like + // doing `set x["foo"] to y.` can. + public override bool SetSuffix(string suffixName, object value, bool failOkay = false) + { + if (base.SetSuffix(suffixName, value, true)) + return true; + + // If the above fails, then fallback on the key technique: + internalDictionary[new StringValue(suffixName)] = FromPrimitiveWithAssert(value); + return true; + } + + // Try to get the suffix the normal way that all structures do, but if + // that fails, then try to get the value in the lexicon who's key is + // this suffix name. (This implements using keys with the "colon" suffix + // syntax for issue #2551.) + public override ISuffixResult GetSuffix(string suffixName, bool failOkay = false) + { + ISuffixResult baseResult = base.GetSuffix(suffixName, true); + if (baseResult != null) + return baseResult; + + // If the above fails, but this suffix IS the name of a key in the + // dictionary, then try to use the key-suffix we made earlier + // (or make a new one and use it now) + // --------------------------------------------------------------- + + StringValue suffixAsStruct = new StringValue(suffixName); + + if (internalDictionary.ContainsKey(suffixAsStruct)) // even if keySuffixes has the value, it doesn't count if the key isn't there anymore. + { + SetSuffix theSuffix; + if (keySuffixes.TryGetValue(suffixAsStruct, out theSuffix)) + { + return theSuffix.Get(); + } + else // make a new suffix then since this is the first time it got mentioned this way: + { + theSuffix = new SetSuffix(() => internalDictionary[suffixAsStruct], value => internalDictionary[suffixAsStruct] = value); + keySuffixes.Add(suffixAsStruct, theSuffix); + return theSuffix.Get(); + } + } + else + { + // This will error out, but we may as well also remove this key + // from the list of suffixes: + keySuffixes.Remove(suffixAsStruct); + + if (failOkay) + return null; + else + throw new KOSSuffixUseException("get", suffixName, this); + } + } + + public override BooleanValue HasSuffix(StringValue suffixName) + { + if (base.HasSuffix(suffixName)) + return true; + if (internalDictionary.ContainsKey(suffixName)) + { + // It can only be a suffix if it is a valid identifier pattern, else the + // parser won't let the colon suffix syntax see it to pass it to GetSuffix() + // or SetSuffix(): + return StringUtil.IsValidIdentifier(suffixName); + } + return false; + } + + /// + /// Like normal Structure.GetSuffixNames except it also adds all + /// the keys that would validly work with the colon suffix syntax + /// to the list. + /// + /// + public override ListValue GetSuffixNames() + { + ListValue theList = base.GetSuffixNames(); + + foreach (Structure key in internalDictionary.Keys) + { + StringValue keyStr = key as StringValue; + if (keyStr != null && StringUtil.IsValidIdentifier(keyStr)) + { + theList.Add(keyStr); + } + } + return new ListValue(theList.OrderBy(item => item.ToString())); + } public override Dump Dump() { var result = new DumpWithHeader diff --git a/src/kOS.Safe/Encapsulation/Structure.cs b/src/kOS.Safe/Encapsulation/Structure.cs index fd8d61dec..302749158 100644 --- a/src/kOS.Safe/Encapsulation/Structure.cs +++ b/src/kOS.Safe/Encapsulation/Structure.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Text; using System.Collections.Generic; @@ -130,19 +130,28 @@ private static IDictionary GetStaticSuffixesForType(Type curren } } - public virtual bool SetSuffix(string suffixName, object value) + /// + /// Set a suffix of this structure that has suffixName to the given value. + /// If failOkay is false then it will throw exception if it fails to find the suffix. + /// If failOkay is true then it will continue happily if it fails to find the suffix. + /// + /// + /// + /// + /// false if failOkay was true and it failed to find the suffix + public virtual bool SetSuffix(string suffixName, object value, bool failOkay = false) { callInitializeSuffixes(); var suffixes = GetStaticSuffixesForType(GetType()); - if (!ProcessSetSuffix(suffixes, suffixName, value)) + if (!ProcessSetSuffix(suffixes, suffixName, value, failOkay)) { - return ProcessSetSuffix(instanceSuffixes, suffixName, value); + return ProcessSetSuffix(instanceSuffixes, suffixName, value, failOkay); } return false; } - private bool ProcessSetSuffix(IDictionary suffixes, string suffixName, object value) + private bool ProcessSetSuffix(IDictionary suffixes, string suffixName, object value, bool failOkay = false) { ISuffix suffix; if (suffixes.TryGetValue(suffixName, out suffix)) @@ -153,12 +162,22 @@ private bool ProcessSetSuffix(IDictionary suffixes, string suff settable.Set(value); return true; } - throw new KOSSuffixUseException("set", suffixName, this); + if (failOkay) + return false; + else + throw new KOSSuffixUseException("set", suffixName, this); } return false; } - public virtual ISuffixResult GetSuffix(string suffixName) + /// + /// Get the suffix with this name, or if it fails to find it, then either + /// throw exception or merely return null. (Will return null only if failOkay is true). + /// + /// + /// + /// + public virtual ISuffixResult GetSuffix(string suffixName, bool failOkay = false) { callInitializeSuffixes(); ISuffix suffix; @@ -171,7 +190,10 @@ public virtual ISuffixResult GetSuffix(string suffixName) if (!suffixes.TryGetValue(suffixName, out suffix)) { - throw new KOSSuffixUseException("get",suffixName,this); + if (failOkay) + return null; + else + throw new KOSSuffixUseException("get",suffixName,this); } return suffix.Get(); } diff --git a/src/kOS.Safe/Execution/CPU.cs b/src/kOS.Safe/Execution/CPU.cs index 50f43ecd5..08534b28b 100644 --- a/src/kOS.Safe/Execution/CPU.cs +++ b/src/kOS.Safe/Execution/CPU.cs @@ -324,6 +324,31 @@ public object PopScopeStack(int howMany) return returnVal; } + /// + /// Find the closest-to-top subroutine context and return its + /// CameFromPriority. Returns current priority if not in a subroutine. + /// + /// + private InterruptPriority CurrentCameFromPriority() + { + bool done = false; + for (int depth = 0; !done; ++depth) + { + object stackItem = stack.PeekScope(depth); + if (stackItem == null) + { + done = true; + } + else + { + SubroutineContext context = stackItem as SubroutineContext; + if (context != null) + return context.CameFromPriority; + } + } + return CurrentPriority; // fallback if none found. + } + private void PopFirstContext() { while (contexts.Count > 1) @@ -465,7 +490,13 @@ public void RunProgram(List program, bool silent) public void BreakExecution(bool manual) { SafeHouse.Logger.Log(string.Format("Breaking Execution {0} Contexts: {1}", manual ? "Manually" : "Automatically", contexts.Count)); - if (contexts.Count > 1) + if (contexts.Count == 0) + { + // Skip most of what this method does, since there's no execution to break. + // This case should only be posisble if BreakExecution() is called while the + // CPU is off or power starved, as can happen during OnLoad(). + } + else if (contexts.Count > 1) { AbortAllYields(); @@ -491,14 +522,15 @@ public void BreakExecution(bool manual) stack.Clear(); } } + CurrentPriority = InterruptPriority.Normal; } else { if (manual) currentContext.ClearTriggers(); // Removes the interpreter's triggers on Control-C and the like, but not on errors. SkipCurrentInstructionId(); + CurrentPriority = InterruptPriority.Normal; } - CurrentPriority = InterruptPriority.Normal; ResetStatistics(); } @@ -594,6 +626,20 @@ public void AssertValidDelegateCall(IUserDelegate userDelegate) } } + /// + /// Allow kOS code to lower the CPU priority (only the CPU is allowed to raise + /// it via its interrupts system, but code is allowed to lower it if it wants). + /// The new priority will be equal to whatever the priority was of the code + /// that got interrupted to get here. (If priority 10 code gets interrupted + /// by priority 20 code, and that priority 20 code calls PrevPriority(), then + /// it will drop to priority 10 because that was the priority of whomever got + /// interrupted to get here.) + /// + public void DropBackPriority() + { + CurrentPriority = CurrentCameFromPriority(); + } + /// /// Return the subroutine call trace of how the code got to where it is right now. /// @@ -785,7 +831,7 @@ public bool VariableIsRemovable(Variable variable) public void RemoveVariable(string identifier) { VariableScope currentScope = GetCurrentScope(); - Variable variable = currentScope.RemoveNested(identifier); + Variable variable = currentScope.RemoveNestedUserVar(identifier); if (variable != null) { // Tell Variable to orphan its old value now. Faster than relying @@ -1439,7 +1485,6 @@ private void ContinueExecution(bool doProfiling) { var executeNext = true; int howManyNormalPriority = 0; - bool okayToActivatePendingTriggers = false; executeLog.Remove(0, executeLog.Length); // In .net 2.0, StringBuilder had no Clear(), which is what this is simulating. while (InstructionsThisUpdate < instructionsPerUpdate && @@ -1451,15 +1496,6 @@ private void ContinueExecution(bool doProfiling) // happen immediately on the next opcode: ProcessTriggers(); - // It is not okay to re-activate pending triggers till all existing active triggers - // of Recurring priority have been flushed out and executed: - if ((! okayToActivatePendingTriggers) && - (! stack.HasDelayingTriggerContexts()) && - ! currentContext.HasActiveTriggersAtLeastPriority(InterruptPriority.Recurring)) - { - okayToActivatePendingTriggers = true; - } - if (IsYielding()) { executeNext = false; @@ -1479,14 +1515,7 @@ private void ContinueExecution(bool doProfiling) // priority with a trigger. ProcessTriggers(); - // As long as all there are no more of the "pending" kinds of trigger - // on the callstack and we have reached at least one opcode of mainline - // code or of immediate trigger code, then it's okay to activate the - // pending triggers now: - if (okayToActivatePendingTriggers) - { - currentContext.ActivatePendingTriggers(); - } + currentContext.ActivatePendingTriggersAbovePriority(CurrentPriority); if (executeLog.Length > 0) SafeHouse.Logger.Log(executeLog.ToString()); diff --git a/src/kOS.Safe/Execution/ICpu.cs b/src/kOS.Safe/Execution/ICpu.cs index fc820a4ab..51c03c75c 100644 --- a/src/kOS.Safe/Execution/ICpu.cs +++ b/src/kOS.Safe/Execution/ICpu.cs @@ -35,6 +35,7 @@ public interface ICpu : IFixedUpdateObserver string DumpStack(); void RemoveVariable(string identifier); int InstructionPointer { get; set; } + void DropBackPriority(); double SessionTime { get; } List ProfileResult { get; } int NextTriggerInstanceId {get; } diff --git a/src/kOS.Safe/Execution/InterruptPriority.cs b/src/kOS.Safe/Execution/InterruptPriority.cs index b8387bf97..225e62fd1 100644 --- a/src/kOS.Safe/Execution/InterruptPriority.cs +++ b/src/kOS.Safe/Execution/InterruptPriority.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace kOS.Safe.Execution { @@ -23,9 +23,13 @@ public enum InterruptPriority : int Normal = 0, /// A one-shot callback such as is common with GUI code CallbackOnce = 10, - /// A trigger that tends to keep getting scheduled to happen + /// A user-made trigger that tends to keep getting scheduled to happen /// again as soon as the previous call to it is completed. - Recurring = 20 + Recurring = 20, + /// These are recurring triggers too, but they absolutely MUST always fire + /// because they are used for the coooked controls like LOCK THROTTLLE and + /// LOCK STEERING + RecurringControl = 30 } } diff --git a/src/kOS.Safe/Execution/ProgramContext.cs b/src/kOS.Safe/Execution/ProgramContext.cs index 939014271..f8cd351c8 100644 --- a/src/kOS.Safe/Execution/ProgramContext.cs +++ b/src/kOS.Safe/Execution/ProgramContext.cs @@ -251,15 +251,15 @@ public bool ContainsTrigger(TriggerInfo trigger) } /// - /// Take all the pending triggers that have been added by AddPendingTrigger, - /// and finally make them become active. To be called by the CPU when it - /// decides that enough mainline code has had a chance to happen that it's - /// okay to enable triggers again. + /// Take only those pending triggers that AddPendingTrigger added who's + /// Priority is higher than the given value, and make them become active. + /// ("active" here means "called on the callstack like a subroutine.") /// - public void ActivatePendingTriggers() + /// + public void ActivatePendingTriggersAbovePriority(InterruptPriority aboveThis) { - Triggers.AddRange(TriggersToInsert); - TriggersToInsert.Clear(); + Triggers.AddRange(TriggersToInsert.FindAll(t => t.Priority > aboveThis)); + TriggersToInsert.RemoveAll(t => t.Priority > aboveThis); } public bool HasActiveTriggersAtLeastPriority(InterruptPriority pri) diff --git a/src/kOS.Safe/Execution/VariableScope.cs b/src/kOS.Safe/Execution/VariableScope.cs index b548753b9..5b9106219 100644 --- a/src/kOS.Safe/Execution/VariableScope.cs +++ b/src/kOS.Safe/Execution/VariableScope.cs @@ -1,4 +1,5 @@ -using System; +using kOS.Safe.Binding; +using System; using System.Collections.Generic; namespace kOS.Safe.Execution @@ -97,20 +98,47 @@ public bool Remove(string name) /// Remove this variable from this local scope OR whichever /// VariableScope it is found in first when doing a scope walk /// up the parent chain to find the first hit. + /// identifier to remove + /// if true, allow bound variables to be removed, else do not. /// - public Variable RemoveNested(string name) + private Variable RemoveNested(string name, bool removeBound) { Variable res = null; if (!Variables.TryGetValue(name, out res)) { - return ParentScope.RemoveNested(name); + return ParentScope.RemoveNested(name, removeBound); } + if (res is BoundVariable && !removeBound) + return null; // If not allowed to remove this bound variable, pretend it wasn't found. + Variables.Remove(name); return res; } + /// + /// Remove this variable from whichever scope it is "most locally" + /// found in, walking all the way up to the global scope. It will + /// only remove USER-made variables, refusing to remove bound + /// variables. It will act like it wasn't found if it finds + /// a bound variable. + /// + /// identifier of the variable to remove + /// the variable that was removed, or null if not found (or a bound var found that doesn't count) + public Variable RemoveNestedUserVar(string name) + { + return RemoveNested(name, false); + } + + // HINT: If there ever is a need for it, this is how we'd have an API to allow + // removing a bound variable. This isn't enabled because it would currently + // be un-used and therefore un-tested. + // public Variable RemoveNestedAnyVar(string name) + // { + // return RemoveNested(name, true); + // } + /// /// True only if this variable name exists in THIS local scope. /// Does NOT perform a walk up the parent scope chain to look diff --git a/src/kOS.Safe/Function/Misc.cs b/src/kOS.Safe/Function/Misc.cs index 67e49afbe..ccaae4cbe 100644 --- a/src/kOS.Safe/Function/Misc.cs +++ b/src/kOS.Safe/Function/Misc.cs @@ -383,4 +383,13 @@ public override void Execute(SafeSharedObjects shared) ReturnValue = new BuiltinDelegate(shared.Cpu, name); } } + + [Function("droppriority")] + public class AllowInterrupt : SafeFunctionBase + { + public override void Execute(SafeSharedObjects shared) + { + shared.Cpu.DropBackPriority(); + } + } } \ No newline at end of file diff --git a/src/kOS.Safe/Properties/AssemblyInfo.cs b/src/kOS.Safe/Properties/AssemblyInfo.cs index 63eb9dd59..55fea6625 100644 --- a/src/kOS.Safe/Properties/AssemblyInfo.cs +++ b/src/kOS.Safe/Properties/AssemblyInfo.cs @@ -31,5 +31,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyFileVersion("1.1.8.0")] -[assembly: AssemblyVersion("1.1.8.0")] +[assembly: AssemblyFileVersion("1.1.9.0")] +[assembly: AssemblyVersion("1.1.9.0")] diff --git a/src/kOS.Safe/Utilities/StringUtil.cs b/src/kOS.Safe/Utilities/StringUtil.cs index 8602d1a89..c587a8ed4 100644 --- a/src/kOS.Safe/Utilities/StringUtil.cs +++ b/src/kOS.Safe/Utilities/StringUtil.cs @@ -1,4 +1,6 @@ -using System; +using System; +using System.Text.RegularExpressions; + namespace kOS.Safe { /// @@ -6,6 +8,11 @@ namespace kOS.Safe /// public static class StringUtil { + // The IDENTIFIER Regex Pattern is taken directly from kRISC.tpg - if it changes there, it should change here too. + // (It's messy to actually use the pattern directly from Scanner.cs because that requires an instance + // of SharedObjects to get an instance of the compiler.) + private static Regex identifierPattern = new Regex(@"\G(?:[_\p{L}]\w*)"); + public static bool EndsWith(string str, string suffix) { int strLen = str.Length; @@ -48,5 +55,16 @@ public static bool StartsWith(string str, string prefix) return true; } + + public static bool IsValidIdentifier(string str) + { + Match match = identifierPattern.Match(str); + + // Only counts as a valid identifier if the entire string matched without + // any leftover characters at the end of it: + if (match.Success && match.Length == str.Length) + return true; + return false; + } } } diff --git a/src/kOS/Function/FunctionBase.cs b/src/kOS/Function/FunctionBase.cs index cbc8cdd46..34de012d0 100644 --- a/src/kOS/Function/FunctionBase.cs +++ b/src/kOS/Function/FunctionBase.cs @@ -1,4 +1,4 @@ -using kOS.Safe.Exceptions; +using kOS.Safe.Exceptions; using kOS.Safe.Function; using kOS.Suffixed; using System; @@ -25,6 +25,16 @@ protected Vector GetVector(object argument) throw new KOSCastException(argument.GetType(), typeof(Vector)); } + protected Direction GetDirection(object argument) + { + var direction = argument as Direction; + if (direction != null) + { + return direction; + } + throw new KOSCastException(argument.GetType(), typeof(Direction)); + } + protected RgbaColor GetRgba(object argument) { var rgba = argument as RgbaColor; diff --git a/src/kOS/Function/Suffixed.cs b/src/kOS/Function/Suffixed.cs index ec25f956b..8772cff45 100644 --- a/src/kOS/Function/Suffixed.cs +++ b/src/kOS/Function/Suffixed.cs @@ -164,11 +164,26 @@ public override void Execute(SharedObjects shared) { string bodyName = PopValueAssert(shared).ToString(); AssertArgBottomAndConsume(shared); - var result = new BodyAtmosphere(VesselUtils.GetBodyByName(bodyName)); + var result = new BodyAtmosphere(VesselUtils.GetBodyByName(bodyName), shared); ReturnValue = result; } } + [Function("bounds")] + public class FunctionBounds : FunctionBase + { + public override void Execute(SharedObjects shared) + { + Vector relMax = GetVector(PopValueAssert(shared)); + Vector relMin = GetVector(PopValueAssert(shared)); + Direction facing = GetDirection(PopValueAssert(shared)); + Vector absOrigin = GetVector(PopValueAssert(shared)); + AssertArgBottomAndConsume(shared); + + ReturnValue = new BoundsValue(relMin, relMax, absOrigin, facing, shared); + } + } + [Function("heading")] public class FunctionHeading : FunctionBase { @@ -389,6 +404,8 @@ public override void Execute(SharedObjects shared) int argc = CountRemainingArgs(shared); // Handle the var args that might be passed in, or give defaults if fewer args: + bool wiping = (argc >= 9) ? Convert.ToBoolean(PopValueAssert(shared)) : true; + bool pointy = (argc >= 8) ? Convert.ToBoolean(PopValueAssert(shared)) : true; double width = (argc >= 7) ? GetDouble(PopValueAssert(shared)) : 0.2; bool show = (argc >= 6) ? Convert.ToBoolean(PopValueAssert(shared)) : false; double scale = (argc >= 5) ? GetDouble(PopValueAssert(shared)) : 1.0; @@ -410,18 +427,33 @@ public override void Execute(SharedObjects shared) Vector start = (startUpdater == null) ? GetVector(argStart) : GetDefaultStart(); AssertArgBottomAndConsume(shared); - DoExecuteWork(shared, start, vec, rgba, str, scale, show, width, colorUpdater, vecUpdater, startUpdater); + DoExecuteWork(shared, start, vec, rgba, str, scale, show, width, pointy, wiping, colorUpdater, vecUpdater, startUpdater); } - public void DoExecuteWork(SharedObjects shared, Vector start, Vector vec, RgbaColor rgba, string str, double scale, bool show, double width, KOSDelegate colorUpdater, KOSDelegate vecUpdater, KOSDelegate startUpdater) - { - var vRend = new VectorRenderer( shared.UpdateHandler, shared ) + public void DoExecuteWork( + SharedObjects shared, + Vector start, + Vector vec, + RgbaColor rgba, + string str, + double scale, + bool show, + double width, + bool pointy, + bool wiping, + KOSDelegate colorUpdater, + KOSDelegate vecUpdater, + KOSDelegate startUpdater) + { + var vRend = new VectorRenderer(shared.UpdateHandler, shared) { Vector = vec, Start = start, Color = rgba, Scale = scale, - Width = width + Width = width, + Pointy = pointy, + Wiping = wiping }; vRend.SetLabel( str ); vRend.SetShow( show ); diff --git a/src/kOS/Logger.cs b/src/kOS/Logger.cs index 91d4024ce..484e103c6 100644 --- a/src/kOS/Logger.cs +++ b/src/kOS/Logger.cs @@ -1,4 +1,4 @@ -using System; +using System; using kOS.Safe; using System.Collections.Generic; using kOS.Suffixed; diff --git a/src/kOS/Module/kOSProcessor.cs b/src/kOS/Module/kOSProcessor.cs index e491d7bbb..42e648c28 100644 --- a/src/kOS/Module/kOSProcessor.cs +++ b/src/kOS/Module/kOSProcessor.cs @@ -548,6 +548,10 @@ private void CalcConstsFromKSP() SafeHouse.Logger.LogError("kOSProcessor: This game installation is badly broken. It appears to have no planets in it."); else ConstantValue.GravConst = anyBody.gravParameter / anyBody.Mass; + + ConstantValue.AvogadroConst = PhysicsGlobals.AvogadroConstant; + ConstantValue.BoltzmannConst = PhysicsGlobals.BoltzmannConstant; + ConstantValue.IdealGasConst = PhysicsGlobals.IdealGasConstant; } private void InitProcessorTracking() diff --git a/src/kOS/Module/kOSVesselModule.cs b/src/kOS/Module/kOSVesselModule.cs index 9abf811ce..c114c6472 100644 --- a/src/kOS/Module/kOSVesselModule.cs +++ b/src/kOS/Module/kOSVesselModule.cs @@ -394,6 +394,11 @@ private void CheckRehookAutopilot() /// private void UpdateAutopilot(FlightCtrlState c) { + // Lock out controls if insufficient avionics in RP-0. + ControlTypes RP0Lock = InputLockManager.GetControlLock("RP0ControlLocker"); + if (RP0Lock != 0) + return; + if (Vessel != null) { if (childParts.Count > 0) diff --git a/src/kOS/Properties/AssemblyInfo.cs b/src/kOS/Properties/AssemblyInfo.cs index 9ba18cf3b..bbdea5209 100644 --- a/src/kOS/Properties/AssemblyInfo.cs +++ b/src/kOS/Properties/AssemblyInfo.cs @@ -31,6 +31,6 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyFileVersion("1.1.8.0")] -[assembly: AssemblyVersion("1.1.8.0")] +[assembly: AssemblyFileVersion("1.1.9.0")] +[assembly: AssemblyVersion("1.1.9.0")] [assembly: KSPAssembly("kOS", 1, 7)] diff --git a/src/kOS/Screen/GUIWindow.cs b/src/kOS/Screen/GUIWindow.cs index a8fa25212..a5f6e30e8 100644 --- a/src/kOS/Screen/GUIWindow.cs +++ b/src/kOS/Screen/GUIWindow.cs @@ -41,6 +41,7 @@ public bool IsForShared(SharedObjects s) public void Awake() { + // Transparent - leave the widget inside it to draw background if it wants to. style = new GUIStyle(HighLogic.Skin.window); style.normal.background = null; @@ -66,6 +67,10 @@ public void Awake() GameEvents.onHideUI.Add (OnHideUI); GameEvents.onShowUI.Add (OnShowUI); + + // Fixes #2568 - Unity IMGUI does its own individual input locking per field that needs it, + // so don't use KSP's more high-level control locking: + OptOutOfControlLocking = true; } public void OnDestroy() diff --git a/src/kOS/Screen/KOSManagedWindow.cs b/src/kOS/Screen/KOSManagedWindow.cs index 26a039de6..5759c8b67 100644 --- a/src/kOS/Screen/KOSManagedWindow.cs +++ b/src/kOS/Screen/KOSManagedWindow.cs @@ -45,6 +45,18 @@ public abstract class KOSManagedWindow : MonoBehaviour private string lockIdName; + private bool optOutOfControlLocking; + /// + /// Fixes #2568 - If the window is one where Unity can handle doing the keyboard focus + /// properly itself, like a Unity IMGUI window, then it should set this to true so it + /// will avoid using KSP's more high level control locking scheme whcih is a bit flaky at times: + /// + public bool OptOutOfControlLocking + { + get { return optOutOfControlLocking; } + set { if (value) InputLockManager.RemoveControlLock(lockIdName); optOutOfControlLocking = value; } + } + protected KOSManagedWindow(string lockIdName = "") { // multiply by 50 so there's a range for future expansion for other GUI objects inside the window: @@ -144,6 +156,8 @@ protected Vector2 MouseButtonDownPosRelative /// public virtual void GetFocus() { + if (OptOutOfControlLocking) + return; if (HighLogic.LoadedSceneIsFlight || HighLogic.LoadedSceneHasPlanetarium) InputLockManager.SetControlLock(ControlTypes.ALLBUTCAMERAS, lockIdName); } @@ -156,6 +170,8 @@ public virtual void GetFocus() /// public virtual void LoseFocus() { + if (OptOutOfControlLocking) + return; InputLockManager.RemoveControlLock(lockIdName); } diff --git a/src/kOS/Screen/KOSToolbarWindow.cs b/src/kOS/Screen/KOSToolbarWindow.cs index e5c4b8a53..4cb9f2058 100644 --- a/src/kOS/Screen/KOSToolbarWindow.cs +++ b/src/kOS/Screen/KOSToolbarWindow.cs @@ -55,6 +55,7 @@ public class KOSToolbarWindow : MonoBehaviour private static GUIStyle vesselNameStyle; private static GUIStyle partNameStyle; private static GUIStyle tooltipLabelStyle; + private static GUIStyle smallLabelStyle; private static GUIStyle boxDisabledStyle; private static GUIStyle boxOffStyle; private static GUIStyle boxOnStyle; @@ -447,9 +448,9 @@ public void DrawWindow(int windowID) CountBeginVertical("", 150); GUILayout.Label("CONFIG VALUES", headingLabelStyle); - GUILayout.Label("To access other settings, see the kOS section in KSP's difficulty settings.", tooltipLabelStyle); + GUILayout.Label("To access other settings, see the kOS section in KSP's difficulty settings.", smallLabelStyle); GUILayout.Label("Global VALUES", headingLabelStyle); - GUILayout.Label("Changes to these settings are saved and globally affect all saved games.", tooltipLabelStyle); + GUILayout.Label("Changes to these settings are saved and globally affect all saved games.", smallLabelStyle); int whichInt = 0; // increments only when an integer field is encountered in the config keys, else stays put. @@ -619,7 +620,7 @@ private int DrawConfigIntField(int keyVal, int whichInt) string fieldValue = (backInt == 0) ? "" : backInt.ToString(); // this lets the user temporarily delete the whole value instead of having it become a zero. GUI.SetNextControlName(fieldName); - fieldValue = GUILayout.TextField(fieldValue, 6, panelSkin.textField, GUILayout.MinWidth(60)); + fieldValue = GUILayout.TextField(fieldValue, 4, panelSkin.textField, GUILayout.MinWidth(60)); fieldValue = fieldValue.Trim(' '); int newInt = -99; // Nonzero value to act as a flag to detect if the following line got triggered: @@ -913,7 +914,15 @@ private static GUISkin BuildPanelSkin() { fontSize = 11, padding = new RectOffset(0, 2, 0, 2), - normal = { textColor = Color.white } + normal = { textColor = Color.white }, + wordWrap = false + }; + smallLabelStyle = new GUIStyle(theSkin.label) + { + fontSize = 11, + padding = new RectOffset(0, 2, 0, 2), + normal = { textColor = Color.white }, + wordWrap = true }; partNameStyle = new GUIStyle(theSkin.box) { diff --git a/src/kOS/Screen/TermWindow.cs b/src/kOS/Screen/TermWindow.cs index e0e88d622..215d89165 100644 --- a/src/kOS/Screen/TermWindow.cs +++ b/src/kOS/Screen/TermWindow.cs @@ -1050,6 +1050,7 @@ internal void AttachTo(SharedObjects sharedObj) shared.Screen.Brightness = SafeHouse.Config.TerminalBrightness; formerCharPixelWidth = shared.Screen.CharacterPixelWidth; formerCharPixelHeight = shared.Screen.CharacterPixelHeight; + shared.Screen.SetSize(SafeHouse.Config.TerminalDefaultHeight, SafeHouse.Config.TerminalDefaultWidth); NotifyOfScreenResize(shared.Screen); shared.Screen.AddResizeNotifier(NotifyOfScreenResize); diff --git a/src/kOS/Suffixed/BodyAtmosphere.cs b/src/kOS/Suffixed/BodyAtmosphere.cs index 27610de33..c4ddc7390 100644 --- a/src/kOS/Suffixed/BodyAtmosphere.cs +++ b/src/kOS/Suffixed/BodyAtmosphere.cs @@ -1,4 +1,4 @@ -using kOS.Safe.Encapsulation; +using kOS.Safe.Encapsulation; using kOS.Safe.Encapsulation.Suffixes; using kOS.Safe.Exceptions; @@ -8,10 +8,12 @@ namespace kOS.Suffixed public class BodyAtmosphere : Structure { private readonly CelestialBody celestialBody; + private readonly SharedObjects shared; - public BodyAtmosphere(CelestialBody celestialBody) + public BodyAtmosphere(CelestialBody celestialBody, SharedObjects shared) { this.celestialBody = celestialBody; + this.shared = shared; AddSuffix("BODY", new Suffix(()=> celestialBody.bodyName)); AddSuffix("EXISTS", new Suffix(()=> celestialBody.atmosphere)); @@ -19,8 +21,9 @@ public BodyAtmosphere(CelestialBody celestialBody) AddSuffix("SEALEVELPRESSURE", new Suffix(()=> celestialBody.atmosphere ? celestialBody.atmospherePressureSeaLevel * ConstantValue.KpaToAtm : 0)); AddSuffix("HEIGHT", new Suffix(()=> celestialBody.atmosphere ? celestialBody.atmosphereDepth : 0)); AddSuffix("ALTITUDEPRESSURE", new OneArgsSuffix((alt)=> celestialBody.GetPressure(alt) * ConstantValue.KpaToAtm)); - - AddSuffix("SCALE", new Suffix(() => { throw new KOSAtmosphereObsoletionException("0.17.2","SCALE","",string.Empty); })); + AddSuffix("MOLARMASS", new Suffix(() => celestialBody.atmosphereMolarMass)); + AddSuffix(new string[] { "ADIABATICINDEX", "ADBIDX" }, new Suffix(() => celestialBody.atmosphereAdiabaticIndex)); + AddSuffix(new string[] { "ALTITUDETEMPERATURE", "ALTTEMP" }, new OneArgsSuffix((alt) => celestialBody.GetTemperature(alt))); } public override string ToString() diff --git a/src/kOS/Suffixed/BodyTarget.cs b/src/kOS/Suffixed/BodyTarget.cs index d6df150dd..b37ec03dd 100644 --- a/src/kOS/Suffixed/BodyTarget.cs +++ b/src/kOS/Suffixed/BodyTarget.cs @@ -103,7 +103,7 @@ private void BodyInitializeSuffixes() AddSuffix("RADIUS", new Suffix(() => Body.Radius)); AddSuffix("MU", new Suffix(() => Body.gravParameter)); AddSuffix("ROTATIONPERIOD", new Suffix(() => Body.rotationPeriod)); - AddSuffix("ATM", new Suffix(() => new BodyAtmosphere(Body))); + AddSuffix("ATM", new Suffix(() => new BodyAtmosphere(Body, Shared))); AddSuffix("ANGULARVEL", new Suffix(() => RawAngularVelFromRelative(Body.angularVelocity))); AddSuffix("SOIRADIUS", new Suffix(() => Body.sphereOfInfluence)); AddSuffix("ROTATIONANGLE", new Suffix(() => Body.rotationAngle)); @@ -226,6 +226,24 @@ public ScalarValue AltitudeFromPosition(Vector position) return Body.GetAltitude(unityWorldPosition); } + /// + /// Interpret the vector given as a 3D position, and return the altitude above terrain unless + /// that terrain is below sea level on a world that has a sea, in which case return the sea + /// level atitude instead, similar to how radar altitude is displayed to the player. + /// + /// + /// + public ScalarValue RadarAltitudeFromPosition(Vector position) + { + GeoCoordinates geo = GeoCoordinatesFromPosition(position); + ScalarValue terrainHeight = geo.GetTerrainAltitude(); + ScalarValue seaAlt = AltitudeFromPosition(position); + if (Body.ocean && terrainHeight < 0) + return seaAlt; + else + return seaAlt - terrainHeight; + } + /// /// Annoyingly, KSP returns CelestialBody.angularVelociy in a frame of reference /// relative to the ship facing instead of the universe facing. This would be @@ -246,10 +264,10 @@ public double GetDistance() return Vector3d.Distance(Shared.Vessel.CoMD, Body.position) - Body.Radius; } - public override ISuffixResult GetSuffix(string suffixName) + public override ISuffixResult GetSuffix(string suffixName, bool failOkay) { if (Target == null) throw new Exception("BODY structure appears to be empty!"); - return base.GetSuffix(suffixName); + return base.GetSuffix(suffixName, failOkay); } public override string ToString() diff --git a/src/kOS/Suffixed/BoundsValue.cs b/src/kOS/Suffixed/BoundsValue.cs new file mode 100644 index 000000000..5f6e6b670 --- /dev/null +++ b/src/kOS/Suffixed/BoundsValue.cs @@ -0,0 +1,215 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; +using kOS.Utilities; +using kOS; +using kOS.Safe.Encapsulation; +using kOS.Safe.Encapsulation.Suffixes; +using kOS.Safe; + +namespace kOS.Suffixed +{ + /// + /// BoundsValue is kOS's wrapper around Unity's Bounds object for making a + /// bounding box aligned to some axis. A bounding box MUST be allinged to + /// some set of axes, and the BoundsValue won't remember what those axes + /// are- it will depend on how you created it (i.e. if it's the bounds of + /// a whole rocket, then it's oriented to ship:facing, but if it's the + /// bounds of a single part then it's oriented to that part's part:facing.) + /// One important difference between Unity's Bounds object + /// and kOS's BoundsValue is that kOS will store the "anchor point" that + /// is offcenter within the bounds, and return Min/Max values relative to + /// THAT anchor point instead of relative to the box's center like default + /// Unity does. This is so that part bounds values will make sense relative + /// to the anchor point of the part (i.e. where the part's part.transform is, + /// which isn't always centered within the mesh.) + /// + [kOS.Safe.Utilities.KOSNomenclature("Bounds")] + public class BoundsValue : Structure + { + public delegate Vector OriginUpdater(); + public delegate Direction FacingUpdater(); + + private SharedObjects shared; + private Bounds unityBounds; + /// + /// This describes where the Unity's bounds box's (0,0,0) origin is anchored to the + /// world coordinates. This is in *world* space, in *world* orientation, NOT in the + /// bounding box's own orientation. + /// + private Vector origin; + private OriginUpdater originDel; + private FacingUpdater facingDel; + private Direction facing; + + /// + /// This variante of the BoundsValue constructor makes a fixed origin and direction that + /// won't properly update when the universe's world axes rotate, or the object in question + /// the bounding box surrounds moves. + /// + /// + /// + /// + /// + public BoundsValue(Vector min, Vector max, Vector origin, Direction facing, SharedObjects shared) + { + unityBounds = new Bounds(); + unityBounds.SetMinMax(min.ToVector3(), max.ToVector3()); + this.origin = origin; + this.shared = shared; + this.facing = facing; + RegisterInitializer(InitializeSuffixes); + } + + /// + /// If this variant of the BoundsValue constructor is used, then the origin and direction will always + /// get updated every time a user gets a suffix that needs them. But if the user ever directly sets the + /// origin or direction, that will stop the updater delegate and turn off this functionality. + /// + /// + /// + /// + /// + public BoundsValue(Vector min, Vector max, OriginUpdater originDel, FacingUpdater facingDel, SharedObjects shared) : + this(min, max, originDel(), facingDel(), shared) + { + this.originDel = originDel; + this.facingDel = facingDel; + } + + public BoundsValue(BoundsValue boundsVal, SharedObjects shared) : + this(new Vector(boundsVal.unityBounds.min), new Vector(boundsVal.unityBounds.max), boundsVal.origin, boundsVal.facing, shared) + { + this.originDel = boundsVal.originDel; + this.facingDel = boundsVal.facingDel; + } + + /// + /// Just gets the raw Unity bounds object that this wraps. + /// + /// + public Bounds GetUnityBounds() + { + return unityBounds; + } + + public Vector RelativeMin + { + get { return new Vector(unityBounds.min); } + set { unityBounds.min = value.ToVector3(); } + } + + public Vector AbsoluteMin + { + get { return Origin + Facing * new Vector(unityBounds.min); } + } + + public Vector RelativeMax + { + get { return new Vector(unityBounds.max); } + set { unityBounds.max = value.ToVector3(); } + } + + public Vector AbsoluteMax + { + get { return Origin + Facing * new Vector(unityBounds.max); } + } + + public Vector RelativeCenter + { + get { return new Vector(unityBounds.center); } + } + + public Vector AbsoluteCenter + { + get { return new Vector(Facing.Rotation * unityBounds.center + Origin.ToVector3()); } + } + + public Vector Origin + { + get + { + if (originDel != null) origin = originDel(); + return origin; + } + set + { + originDel = null; // disengage the auto-updater when the user sets their own new value. + origin = value; + } + } + + public Direction Facing + { + get + { + if (facingDel != null) + facing = facingDel(); + return facing; + } + set + { + facingDel = null; // disengage the auto-updater when the user sets their own new value. + facing = value; + } + } + + /// + /// Get which of the 8 corners of the bounds box is most "in the direction of" the ray given. + /// i.e. if passed in the up:vector, it will get the topmost corner and if passed in the + /// -up:vector, it will get the bottommost corner. Corner is returned in Ship-Raw ref frame. + /// + /// + /// + public Vector FurthestCorner(Vector ray) + { + // Rotate the ray into the bounds box's local frame axes and that will make it clear which corner to use: + // If that ray has a +x component, then you want the max X, if it has a -x component, then you want the min x, + // etc for each of the 3 axes: + Vector3 orientedRay = Facing.Rotation.Inverse() * ray.Normalized().ToVector3(); + float relX = orientedRay.x > 0 ? unityBounds.max.x : unityBounds.min.x; + float relY = orientedRay.y > 0 ? unityBounds.max.y : unityBounds.min.y; + float relZ = orientedRay.z > 0 ? unityBounds.max.z : unityBounds.min.z; + Vector3 relCorner = new Vector3(relX, relY, relZ); + return Origin + new Vector(Facing.Rotation * relCorner); + } + + public double BottomAltitude() + { + BodyTarget body = BodyTarget.CreateOrGetExisting(shared.Vessel.mainBody, shared); + Vector bottomInShipRaw = FurthestCorner(new Vector(-shared.Vessel.upAxis)); + return body.AltitudeFromPosition(bottomInShipRaw); + } + + public double BottomAltitudeRadar() + { + BodyTarget body = BodyTarget.CreateOrGetExisting(shared.Vessel.mainBody, shared); + Vector bottomInShipRaw = FurthestCorner(new Vector(-shared.Vessel.upAxis)); + return body.RadarAltitudeFromPosition(bottomInShipRaw); + } + + public void InitializeSuffixes() + { + AddSuffix("ABSORIGIN", new SetSuffix(() => Origin, value => Origin = value)); + AddSuffix("FACING", new SetSuffix(() => Facing, value => Facing = value)); + AddSuffix("RELMIN", new SetSuffix(() => RelativeMin, value => RelativeMin = value)); + AddSuffix("RELMAX", new SetSuffix(() => RelativeMax, value => RelativeMax = value)); + AddSuffix("ABSMIN", new NoArgsSuffix(() => AbsoluteMin)); + AddSuffix("ABSMAX", new NoArgsSuffix(() => AbsoluteMax)); + AddSuffix("RELCENTER", new NoArgsSuffix(() => RelativeCenter)); + AddSuffix("ABSCENTER", new NoArgsSuffix(() => AbsoluteCenter)); + AddSuffix("EXTENTS", new SetSuffix(() => new Vector(unityBounds.extents), value => unityBounds.extents = value.ToVector3())); + AddSuffix("SIZE", new SetSuffix(() => new Vector(unityBounds.size), value => unityBounds.size = value.ToVector3())); + AddSuffix("FURTHESTCORNER", new OneArgsSuffix((ray) => FurthestCorner(ray))); + AddSuffix("BOTTOMALT", new NoArgsSuffix(() => BottomAltitude())); + AddSuffix("BOTTOMALTRADAR", new NoArgsSuffix(() => BottomAltitudeRadar())); + } + + public override string ToString() + { + return string.Format("Bounds: ABSORIGIN = {0}, FACING = {1}, RELMIN = {2}, RELMAX = {3}", Origin, Facing, RelativeMin, RelativeMax); + } + } +} diff --git a/src/kOS/Suffixed/Config.cs b/src/kOS/Suffixed/Config.cs index e132a59e2..bad4005d7 100644 --- a/src/kOS/Suffixed/Config.cs +++ b/src/kOS/Suffixed/Config.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using kOS.Safe.Encapsulation; @@ -29,6 +29,8 @@ public class Config : Structure, IConfig public string TelnetIPAddrString { get { return GetPropValue(PropId.TelnetIPAddrString); } set { SetPropValue(PropId.TelnetIPAddrString, value); } } public int TerminalFontDefaultSize {get { return GetPropValue(PropId.TerminalFontDefaultSize); } set { SetPropValue(PropId.TerminalFontDefaultSize, value); } } public string TerminalFontName {get { return GetPropValue(PropId.TerminalFontName); } set { SetPropValue(PropId.TerminalFontName, value); } } + public int TerminalDefaultWidth { get { return GetPropValue(PropId.TerminalDefaultWidth); } set { SetPropValue(PropId.TerminalDefaultWidth, value); } } + public int TerminalDefaultHeight { get { return GetPropValue(PropId.TerminalDefaultHeight); } set { SetPropValue(PropId.TerminalDefaultHeight, value); } } public bool UseBlizzyToolbarOnly { get { return kOSCustomParameters.Instance.useBlizzyToolbarOnly; } set { kOSCustomParameters.Instance.useBlizzyToolbarOnly = value; } } public bool DebugEachOpcode { get { return kOSCustomParameters.Instance.debugEachOpcode; } set { kOSCustomParameters.Instance.debugEachOpcode = value; } } @@ -61,6 +63,8 @@ private void InitializeSuffixes() AddSuffix("BLIZZY", new SetSuffix(() => UseBlizzyToolbarOnly, value => UseBlizzyToolbarOnly = value)); AddSuffix("BRIGHTNESS", new ClampSetSuffix(() => TerminalBrightness, value => TerminalBrightness = value, 0f, 1f, 0.01f)); AddSuffix("DEFAULTFONTSIZE", new ClampSetSuffix(() => TerminalFontDefaultSize, value => TerminalFontDefaultSize = value, 6f, 30f, 1f)); + AddSuffix("DEFAULTWIDTH", new ClampSetSuffix(() => TerminalDefaultWidth, value => TerminalDefaultWidth = value, 15f, 255f, 1f)); + AddSuffix("DEFAULTHEIGHT", new ClampSetSuffix(() => TerminalDefaultHeight, value => TerminalDefaultHeight = value, 3f, 160f, 1f)); } private void BuildValuesDictionary() @@ -71,6 +75,8 @@ private void BuildValuesDictionary() AddConfigKey(PropId.TerminalFontDefaultSize, new ConfigKey("TerminalFontDefaultSize", "DEFAULTFONTSIZE", "Initial Terminal:CHARHEIGHT when a terminal is first opened", 12, 6, 20, typeof(int))); AddConfigKey(PropId.TerminalFontName, new ConfigKey("TerminalFontName", "FONTNAME", "Font Name for terminal window", "_not_chosen_yet_", "n/a", "n/a", typeof(string))); AddConfigKey(PropId.TerminalBrightness, new ConfigKey("TerminalBrightness", "BRIGHTNESS", "Initial brightness setting for new terminals", 0.7d, 0d, 1d, typeof(double))); + AddConfigKey(PropId.TerminalDefaultWidth, new ConfigKey("TerminalDefaultWidth", "DEFAULTWIDTH", "Initial Terminal:WIDTH when a terminal is first opened", 50, 15, 255, typeof(int))); + AddConfigKey(PropId.TerminalDefaultHeight, new ConfigKey("TerminalDefaultHeight", "DEFAULTHEIGHT", "Initial Terminal:HEIGHT when a terminal is first opened", 36, 3, 160, typeof(int))); } private void AddConfigKey(PropId id, ConfigKey key) @@ -149,7 +155,7 @@ private void SaveConfigKey(ConfigKey key, PluginConfiguration config) config.SetValue(key.StringKey, keys[key.StringKey.ToUpper()].Value); } - public override ISuffixResult GetSuffix(string suffixName) + public override ISuffixResult GetSuffix(string suffixName, bool failOkay = false) { ConfigKey key = null; @@ -162,10 +168,18 @@ public override ISuffixResult GetSuffix(string suffixName) key = alias[suffixName]; } - return key != null ? new SuffixResult(FromPrimitiveWithAssert(key.Value)) : base.GetSuffix(suffixName); + return key != null ? new SuffixResult(FromPrimitiveWithAssert(key.Value)) : base.GetSuffix(suffixName, failOkay); } - public override bool SetSuffix(string suffixName, object value) + /// + /// same as Structure.SetSuffix, but it has the extra logic to alter the config keys + /// that the game auto-saves every so often. + /// + /// + /// + /// + /// + public override bool SetSuffix(string suffixName, object value, bool failOkay = false) { ConfigKey key = null; @@ -178,7 +192,7 @@ public override bool SetSuffix(string suffixName, object value) key = alias[suffixName]; } - if (key == null) return base.SetSuffix(suffixName, value); + if (key == null) return base.SetSuffix(suffixName, value, failOkay); if (value.GetType() == key.ValType) { @@ -224,7 +238,9 @@ private enum PropId DebugEachOpcode = 14, TerminalFontDefaultSize = 15, TerminalFontName = 16, - TerminalBrightness = 17 + TerminalBrightness = 17, + TerminalDefaultWidth = 18, + TerminalDefaultHeight = 19 } } } diff --git a/src/kOS/Suffixed/FlightControl.cs b/src/kOS/Suffixed/FlightControl.cs index a81a459a7..9af551a58 100644 --- a/src/kOS/Suffixed/FlightControl.cs +++ b/src/kOS/Suffixed/FlightControl.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Globalization; using System.Collections.Generic; using kOS.AddOns.RemoteTech; @@ -54,7 +54,7 @@ public FlightControl(Vessel vessel) public Vessel Vessel { get; private set; } - public override bool SetSuffix(string suffixName, object value) + public override bool SetSuffix(string suffixName, object value, bool failOkay = false) { float floatValue = 0; Vector vectorValue = null; diff --git a/src/kOS/Suffixed/KUniverseValue.cs b/src/kOS/Suffixed/KUniverseValue.cs index 132a11365..1952facd2 100644 --- a/src/kOS/Suffixed/KUniverseValue.cs +++ b/src/kOS/Suffixed/KUniverseValue.cs @@ -146,7 +146,7 @@ public void ForceSetActiveVessel(VesselTarget vesselTarget) public ScalarValue GetHoursPerDay() { - return GameSettings.KERBIN_TIME ? TimeSpan.HOURS_IN_KERBIN_DAY : TimeSpan.HOURS_IN_EARTH_DAY; + return KSPUtil.dateTimeFormatter.Day / KSPUtil.dateTimeFormatter.Hour; } public void DebugLog(StringValue message) diff --git a/src/kOS/Suffixed/Part/PartValue.cs b/src/kOS/Suffixed/Part/PartValue.cs index 219a053e8..ece3570c2 100644 --- a/src/kOS/Suffixed/Part/PartValue.cs +++ b/src/kOS/Suffixed/Part/PartValue.cs @@ -6,6 +6,7 @@ using kOS.Utilities; using System; using System.Linq; +using System.Collections.Generic; using kOS.Safe.Compilation.KS; using UnityEngine; @@ -46,9 +47,10 @@ private void PartInitializeSuffixes() AddSuffix("CID", new Suffix(() => Part.craftID.ToString())); AddSuffix("UID", new Suffix(() => Part.flightID.ToString())); AddSuffix("ROTATION", new Suffix(() => new Direction(Part.transform.rotation))); - AddSuffix("POSITION", new Suffix(() => new Vector(Part.transform.position - Shared.Vessel.CoMD))); + AddSuffix("POSITION", new Suffix(() => GetPosition())); AddSuffix("TAG", new SetSuffix(GetTagName, SetTagName)); - AddSuffix("FACING", new Suffix(() => GetFacing(Part))); + AddSuffix("FACING", new Suffix(() => GetFacing())); + AddSuffix("BOUNDS", new Suffix(GetBoundsValue)); AddSuffix("RESOURCES", new Suffix(() => GatherResources(Part))); AddSuffix("TARGETABLE", new Suffix(() => Part.Modules.OfType().Any())); AddSuffix("SHIP", new Suffix(() => VesselTarget.CreateOrGetExisting(Part.vessel, Shared))); @@ -67,6 +69,49 @@ private void PartInitializeSuffixes() AddSuffix("HASPHYSICS", new Suffix(() => Part.HasPhysics(), "Is this a strange 'massless' part")); } + public BoundsValue GetBoundsValue() + { + // Our normal facings use Z for forward, but parts use Y for forward: + Quaternion rotateYToZ = Quaternion.FromToRotation(Vector2.up, Vector3.forward); + + Bounds unionBounds = new Bounds(); + + MeshFilter[] meshes = Part.GetComponentsInChildren(); + for (int meshIndex = 0; meshIndex < meshes.Length; ++meshIndex) + { + MeshFilter mesh = meshes[meshIndex]; + Bounds bounds = mesh.mesh.bounds; + + // Part meshes could be scaled as well as rotated (the mesh might describe a + // part that's 1 meter wide while the real part is 2 meters wide, and has a scale of 2x + // encoded into its transform to do this). Because of this, the only really + // reliable way to get the real shape is to let the transform do its work on all 6 corners + // of the bounding box, transforming them with the mesh's transform, then back-calculating + // from that world-space result back into the part's own reference frame to get the bounds + // relative to the part. + Vector3 center = bounds.center; + + // This triple-nested loop visits all 8 corners of the box: + for (int signX = -1; signX <= 1; signX += 2) // -1, then +1 + for (int signY = -1; signY <= 1; signY += 2) // -1, then +1 + for (int signZ = -1; signZ <= 1; signZ += 2) // -1, then +1 + { + Vector3 corner = center + new Vector3(signX * bounds.extents.x, signY * bounds.extents.y, signZ * bounds.extents.z); + Vector3 worldCorner = mesh.transform.TransformPoint(corner); + Vector3 partCorner = rotateYToZ * Part.transform.InverseTransformPoint(worldCorner); + + // Stretches the bounds we're making (which started at size zero in all axes), + // just big enough to include this corner: + unionBounds.Encapsulate(partCorner); + } + } + + Vector min = new Vector(unionBounds.min); + Vector max = new Vector(unionBounds.max); + return new BoundsValue(min, max, delegate { return GetPosition() + new Vector(Part.boundsCentroidOffset); }, delegate { return GetFacing(); }, Shared); + } + + public void ThrowIfNotCPUVessel() { if (Part.vessel.id != Shared.Vessel.id) @@ -132,14 +177,19 @@ public virtual ITargetable Target } } - private Direction GetFacing(global::Part part) + public Direction GetFacing() { // Our normal facings use Z for forward, but parts use Y for forward: Quaternion rotateZToY = Quaternion.FromToRotation(Vector3.forward, Vector3.up); - Quaternion newRotation = part.transform.rotation * rotateZToY; + Quaternion newRotation = Part.transform.rotation * rotateZToY; return new Direction(newRotation); } + public Vector GetPosition() + { + return new Vector(Part.transform.position - Shared.Vessel.CoMD); + } + private void ControlFrom() { ThrowIfNotCPUVessel(); @@ -208,5 +258,6 @@ public override int GetHashCode() { return !Equals(left, right); } + } } diff --git a/src/kOS/Suffixed/PartModuleField/PartModuleFields.cs b/src/kOS/Suffixed/PartModuleField/PartModuleFields.cs index 20f51d810..ca89fff90 100644 --- a/src/kOS/Suffixed/PartModuleField/PartModuleFields.cs +++ b/src/kOS/Suffixed/PartModuleField/PartModuleFields.cs @@ -1,4 +1,4 @@ -using kOS.Safe.Encapsulation; +using kOS.Safe.Encapsulation; using kOS.Safe.Encapsulation.Suffixes; using kOS.Safe.Exceptions; using kOS.Suffixed.Part; @@ -63,29 +63,6 @@ public override string ToString() return returnValue.ToString(); } - /// - /// Return true if the field in question is editable in the KSP rightclick menu - /// as an in-game tweakable right now. - /// - /// the BaseField from the KSP API - /// true if this has a GUI edit widget on it, false if it doesn't. - private bool IsEditable(BaseField field) - { - return GetFieldControls(field).Count > 0; - } - - /// - /// Get the UI_Controls on a KSPField which are user editable. - /// - /// - /// - private List GetFieldControls(BaseField field) - { - var attribs = new List(); - attribs.AddRange(field.FieldInfo.GetCustomAttributes(true)); - return attribs.OfType().Where(obj => (obj).controlEnabled).ToList(); - } - /// /// Return true if the value given is allowed for the field given. This uses the hints from the GUI /// system to decide what is and isn't allowed. (For example if a GUI slider goes from 10 to 20 at @@ -111,7 +88,19 @@ private bool IsLegalValue(BaseField field, ref Structure newVal, out KOSExceptio Type fType = field.FieldInfo.FieldType; object convertedVal = newVal; - if (!IsEditable(field)) + // Using TryGetFieldUIControl() to obtain the control that goes with this + // field is from advice from TriggerAU, of SQUAD, who gave that advice in + // a forum post when I described the problems we were having with the servo + // parts in Breaking Ground DLC. (There is some kind of work being done here + // that seems to allow one field's ranges to override another's as the servo + // parts need to do. This is work which doesn't seem to happen if you look at + // the KSPField's control ranges directly): + UI_Control control; + if (!partModule.Fields.TryGetFieldUIControl(field.name, out control)) + { + throw new KOSInvalidFieldValueException("Field appears to have no UI control attached so kOS refuses to let a script change it."); + } + if (!control.controlEnabled) { except = new KOSInvalidFieldValueException("Field is read-only"); return false; @@ -133,54 +122,58 @@ private bool IsLegalValue(BaseField field, ref Structure newVal, out KOSExceptio return false; } } - List controls = GetFieldControls(field); - // It's really normal for there to be only one control on a KSPField, but because - // it's technically possible to have more than one according to the structure of - // the API, this loop is here to check all of "them": - foreach (UI_Control control in controls) + // Some of these are subclasses of each other, so don't change this to an if/else. + // It's a series of if's on purpose so it checks all classes the control is derived from. + if (control is UI_Toggle) { - // Some of these are subclasses of each other, so don't change this to an if/else. - // It's a series of if's on purpose so it checks all classes the control is derived from. - if (control is UI_Toggle) - { - // Seems there's nothing to check here, but maybe later there will be? - } - if (control is UI_Label) - { - except = new KOSInvalidFieldValueException("Labels are read-only objects that can't be changed"); - isLegal = false; - } - var vector2 = control as UI_Vector2; - if (vector2 != null) + // Seems there's nothing to check here, but maybe later there will be? + } + if (control is UI_Label) + { + except = new KOSInvalidFieldValueException("Labels are read-only objects that can't be changed"); + isLegal = false; + } + var vector2 = control as UI_Vector2; + if (vector2 != null) + { + // I have no clue what this actually looks like in the UI? What is a + // user editable 2-D vector widget? I've never seen this before. + if (convertedVal != null) { - // I have no clue what this actually looks like in the UI? What is a - // user editable 2-D vector widget? I've never seen this before. - if (convertedVal != null) + var vec2 = (Vector2)convertedVal; + if (vec2.x < vector2.minValueX || vec2.x > vector2.maxValueX || + vec2.y < vector2.minValueY || vec2.y > vector2.maxValueY) { - var vec2 = (Vector2)convertedVal; - if (vec2.x < vector2.minValueX || vec2.x > vector2.maxValueX || - vec2.y < vector2.minValueY || vec2.y > vector2.maxValueY) - { - except = new KOSInvalidFieldValueException("Vector2 is outside of allowed range of values"); - isLegal = false; - } + except = new KOSInvalidFieldValueException("Vector2 is outside of allowed range of values"); + isLegal = false; } } - var range = control as UI_FloatRange; - if (range != null) - { - float val = Convert.ToSingle(convertedVal); - val = KOSMath.ClampToIndent(val, range.minValue, range.maxValue, range.stepIncrement); - convertedVal = Convert.ToDouble(val); - } - if (!isLegal) - break; + } + var range = control as UI_FloatRange; + if (range != null) + { + float val = Convert.ToSingle(convertedVal); + val = KOSMath.ClampToIndent(val, range.minValue, range.maxValue, range.stepIncrement); + convertedVal = Convert.ToDouble(val); } newVal = FromPrimitiveWithAssert(convertedVal); return isLegal; } + protected string GetFieldName(BaseField kspField) + { + return kspField.guiName.Length > 0 ? kspField.guiName : kspField.name; + } + // Note that BaseEvent has the GUIName property which effectively does this already. + protected string GetEventName(BaseEvent kspEvent) + { + return kspEvent.GUIName; + } + protected string GetActionName(BaseAction kspAction) + { + return kspAction.guiName.Length > 0 ? kspAction.guiName : kspAction.name; + } /// /// Return a list of all the strings of all KSPfields registered to this PartModule /// which are currently showing on the part's RMB menu. @@ -194,10 +187,14 @@ protected virtual ListValue AllFields(string formatter) foreach (BaseField field in visibleFields) { - returnValue.Add(new StringValue(string.Format(formatter, - IsEditable(field) ? "settable" : "get-only", - field.guiName.ToLower(), - Utilities.Utils.KOSType(field.FieldInfo.FieldType)))); + UI_Control control; + if ( partModule.Fields.TryGetFieldUIControl(field.name, out control)) + { + returnValue.Add(new StringValue(string.Format(formatter, + control.controlEnabled ? "settable" : "get-only", + GetFieldName(field).ToLower(), + Utilities.Utils.KOSType(field.FieldInfo.FieldType)))); + } } return returnValue; } @@ -215,7 +212,7 @@ protected virtual ListValue AllFieldNames() foreach (BaseField field in visibleFields) { - returnValue.Add(new StringValue(field.guiName.ToLower())); + returnValue.Add(new StringValue(GetFieldName(field).ToLower())); } return returnValue; } @@ -234,12 +231,12 @@ public virtual BooleanValue HasField(StringValue fieldName) /// /// Return the field itself that goes with the name (the BaseField, not the value). /// - /// The case-insensitive guiName of the field. + /// The case-insensitive guiName (or name if guiname is empty) of the field. /// a BaseField - a KSP type that can be used to get the value, or its GUI name or its reflection info. protected BaseField GetField(string cookedGuiName) { return partModule.Fields.Cast(). - FirstOrDefault(field => string.Equals(field.guiName, cookedGuiName, StringComparison.CurrentCultureIgnoreCase)); + FirstOrDefault(field => string.Equals(GetFieldName(field), cookedGuiName, StringComparison.CurrentCultureIgnoreCase)); } /// @@ -257,7 +254,7 @@ private ListValue AllEvents(string formatter) { returnValue.Add(new StringValue(string.Format(formatter, "callable", - kspEvent.guiName.ToLower(), + GetEventName(kspEvent).ToLower(), "KSPEvent"))); } return returnValue; @@ -276,7 +273,7 @@ private ListValue AllEventNames() foreach (BaseEvent kspEvent in visibleEvents) { - returnValue.Add(new StringValue(kspEvent.guiName.ToLower())); + returnValue.Add(new StringValue(GetEventName(kspEvent).ToLower())); } return returnValue; } @@ -295,12 +292,12 @@ public BooleanValue HasEvent(StringValue eventName) /// /// Return the KSP BaseEvent going with the given name. /// - /// The event's case-insensitive guiname. + /// The event's case-insensitive guiname (or name if guiname is empty). /// private BaseEvent GetEvent(string cookedGuiName) { return partModule.Events. - FirstOrDefault(kspEvent => string.Equals(kspEvent.guiName, cookedGuiName, StringComparison.CurrentCultureIgnoreCase)); + FirstOrDefault(kspEvent => string.Equals(GetEventName(kspEvent), cookedGuiName, StringComparison.CurrentCultureIgnoreCase)); } /// @@ -315,7 +312,7 @@ private ListValue AllActions(string formatter) { returnValue.Add(new StringValue(string.Format(formatter, "callable", - kspAction.guiName.ToLower(), + GetActionName(kspAction).ToLower(), "KSPAction"))); } return returnValue; @@ -331,7 +328,7 @@ private ListValue AllActionNames() foreach (BaseAction kspAction in partModule.Actions) { - returnValue.Add(new StringValue(kspAction.guiName.ToLower())); + returnValue.Add(new StringValue(GetActionName(kspAction).ToLower())); } return returnValue; } @@ -344,17 +341,17 @@ private ListValue AllActionNames() /// true if it is on the PartModule, false if it is not public BooleanValue HasAction(StringValue actionName) { - return partModule.Actions.Any(kspAction => string.Equals(kspAction.guiName, actionName, StringComparison.CurrentCultureIgnoreCase)); + return partModule.Actions.Any(kspAction => string.Equals(GetActionName(kspAction), actionName, StringComparison.CurrentCultureIgnoreCase)); } /// /// Return the KSP BaseAction going with the given name. /// - /// The event's case-insensitive guiname. + /// The event's case-insensitive guiname (or name if guiname is empty). /// private BaseAction GetAction(string cookedGuiName) { - return partModule.Actions.FirstOrDefault(kspAction => string.Equals(kspAction.guiName, cookedGuiName, StringComparison.CurrentCultureIgnoreCase)); + return partModule.Actions.FirstOrDefault(kspAction => string.Equals(GetActionName(kspAction), cookedGuiName, StringComparison.CurrentCultureIgnoreCase)); } /// diff --git a/src/kOS/Suffixed/StageValues.cs b/src/kOS/Suffixed/StageValues.cs index a1303520f..964b61cbb 100644 --- a/src/kOS/Suffixed/StageValues.cs +++ b/src/kOS/Suffixed/StageValues.cs @@ -75,12 +75,12 @@ private Lexicon GetResourceDictionary() return resLex; } - public override ISuffixResult GetSuffix(string suffixName) + public override ISuffixResult GetSuffix(string suffixName, bool failOkay = false) { string fixedName; if (!Utils.IsResource(suffixName, out fixedName)) { - return base.GetSuffix(suffixName); + return base.GetSuffix(suffixName, failOkay); } double resourceAmount = GetResourceOfCurrentStage(fixedName); diff --git a/src/kOS/Suffixed/Timespan.cs b/src/kOS/Suffixed/Timespan.cs index 88cb90526..3f563e5fc 100644 --- a/src/kOS/Suffixed/Timespan.cs +++ b/src/kOS/Suffixed/Timespan.cs @@ -13,20 +13,11 @@ public class TimeSpan : SerializableStructure, IComparable public const string DumpSpan = "span"; double span; - private const int DAYS_IN_YEAR = 365; - public const int HOURS_IN_EARTH_DAY = 24; - public const int HOURS_IN_KERBIN_DAY = 6; - - private const int MINUTE_IN_HOUR = 60; - private const int SECONDS_IN_MINUTE = 60; - - private const int SECONDS_IN_KERBIN_HOUR = MINUTE_IN_HOUR * SECONDS_IN_MINUTE; - private const int SECONDS_IN_KERBIN_DAY = SECONDS_IN_KERBIN_HOUR * HOURS_IN_KERBIN_DAY; - private const int SECONDS_IN_KERBIN_YEAR = SECONDS_IN_KERBIN_DAY * DAYS_IN_YEAR; - private const int SECONDS_IN_EARTH_HOUR = MINUTE_IN_HOUR * SECONDS_IN_MINUTE; - private const int SECONDS_IN_EARTH_DAY = SECONDS_IN_EARTH_HOUR * HOURS_IN_EARTH_DAY; - private const int SECONDS_IN_EARTH_YEAR = SECONDS_IN_EARTH_DAY * DAYS_IN_YEAR; + private int SecondsPerDay { get { return KSPUtil.dateTimeFormatter.Day; } } + private int SecondsPerHour { get { return KSPUtil.dateTimeFormatter.Hour; } } + private int SecondsPerYear { get { return KSPUtil.dateTimeFormatter.Year; } } + private int SecondsPerMinute { get { return KSPUtil.dateTimeFormatter.Minute; } } // Only used by CreateFromDump() and the other constructors. // Don't make it public because it leaves fields @@ -63,20 +54,12 @@ private void InitializeSuffixes() private ScalarValue CalculateYear() { - if (GameSettings.KERBIN_TIME) - { - return (int)Math.Floor(span / SECONDS_IN_KERBIN_YEAR) + 1; - } - return (int)Math.Floor(span / SECONDS_IN_EARTH_YEAR) + 1; + return (int)Math.Floor(span / SecondsPerYear) + 1; } - private int SecondsPerDay { get { return GameSettings.KERBIN_TIME ? SECONDS_IN_KERBIN_DAY : SECONDS_IN_EARTH_DAY; } } - private int SecondsPerHour { get { return GameSettings.KERBIN_TIME ? SECONDS_IN_KERBIN_HOUR : SECONDS_IN_EARTH_HOUR; } } - private int SecongsPerYear { get { return GameSettings.KERBIN_TIME ? SECONDS_IN_KERBIN_YEAR : SECONDS_IN_EARTH_YEAR; } } - private ScalarValue CalculateDay() { - return (int)Math.Floor(span % SecongsPerYear / SecondsPerDay) + 1; + return (int)Math.Floor(span % SecondsPerYear / SecondsPerDay) + 1; } private ScalarValue CalculateHour() @@ -86,12 +69,12 @@ private ScalarValue CalculateHour() private ScalarValue CalculateMinute() { - return (int)Math.Floor(span % SecondsPerHour / SECONDS_IN_MINUTE); + return (int)Math.Floor(span % SecondsPerHour / SecondsPerMinute); } private ScalarValue CalculateSecond() { - return (int)Math.Floor(span % SECONDS_IN_MINUTE); + return (int)Math.Floor(span % SecondsPerMinute); } public double ToUnixStyleTime() diff --git a/src/kOS/Suffixed/VectorRenderer.cs b/src/kOS/Suffixed/VectorRenderer.cs index 2a1ac8964..fc2e9bc1b 100644 --- a/src/kOS/Suffixed/VectorRenderer.cs +++ b/src/kOS/Suffixed/VectorRenderer.cs @@ -18,6 +18,8 @@ public class VectorRenderer : Structure, IUpdateObserver, IKOSScopeObserver public Vector3d Start { get; set; } public double Scale { get; set; } public double Width { get; set; } + public bool Pointy { get; set; } + public bool Wiping { get; set; } private LineRenderer line; private LineRenderer hat; @@ -65,6 +67,8 @@ public VectorRenderer(UpdateHandler updateHand, SharedObjects shared) Start = new Vector3d(0, 0, 0); Scale = 1.0; Width = 0; + Pointy = false; + Wiping = false; updateHandler = updateHand; this.shared = shared; @@ -260,6 +264,9 @@ private void InitializeSuffixes() Width = value; RenderPointCoords(); })); + + AddSuffix("POINTY", new SetSuffix(() => Pointy, value => Pointy = value)); + AddSuffix("WIPING", new SetSuffix(() => Wiping, value => Wiping = value)); } /// @@ -366,7 +373,7 @@ public void SetShow(BooleanValue newShowVal) } updateHandler.AddObserver(this); line.enabled = true; - hat.enabled = true; + hat.enabled = Pointy; label.enabled = true; } else @@ -437,6 +444,9 @@ public void RenderPointCoords() mapWidthMult = Math.Max(camLookVec.magnitude, 100.0f) / 100.0f; } + // From point1 to point3 is the vector. + // point2 is the spot just short of point3 to start drawing + // the pointy hat, if Pointy is enabled: Vector3d point1 = mapLengthMult * Start; Vector3d point2 = mapLengthMult * (Start + (Scale * 0.95 * Vector)); Vector3d point3 = mapLengthMult * (Start + (Scale * Vector)); @@ -450,9 +460,9 @@ public void RenderPointCoords() line.startWidth = useWidth; line.endWidth = useWidth; line.SetPosition(0, point1); - line.SetPosition(1, point2); + line.SetPosition(1, Pointy ? point2 : point3 ); - // Position the arrow hat: + // Position the arrow hat. Note, if Pointy = false, this will be invisible. hat.positionCount = 2; hat.startWidth = useWidth * 3.5f; hat.endWidth = 0.0f; @@ -479,8 +489,9 @@ public void RenderColor() if (line != null && hat != null) { - // The line has the fade effect from color c1 to color c2: - line.startColor = c1; + // If Wiping, then the line has the fade effect from color c1 to color c2, + // else it stays at c2 the whole way: + line.startColor = (Wiping ? c1 : c2); line.endColor = c2; // The hat does not have the fade effect, staying at color c2 the whole way: hat.startColor = c2; diff --git a/src/kOS/Suffixed/VesselAlt.cs b/src/kOS/Suffixed/VesselAlt.cs index 81d166446..47e898603 100644 --- a/src/kOS/Suffixed/VesselAlt.cs +++ b/src/kOS/Suffixed/VesselAlt.cs @@ -1,4 +1,4 @@ -using kOS.Safe.Encapsulation; +using kOS.Safe.Encapsulation; using kOS.Safe.Encapsulation.Suffixes; using System; using UnityEngine; @@ -34,10 +34,20 @@ public ScalarValue GetPeriapsis() public ScalarValue GetRadar() { - return Convert.ToDouble( - shared.Vessel.heightFromTerrain > 0 ? - Mathf.Min(shared.Vessel.heightFromTerrain, (float)shared.Vessel.altitude) : - (float)shared.Vessel.altitude); + double seaAlt = shared.Vessel.altitude; + + // Note, this is -1 when ground is too far away, which is why it needs the "> 0" checks you see below + // to fallback on sea level altitude when it isn't working. + double groundAlt = shared.Vessel.heightFromTerrain; + + if (shared.Vessel.mainBody.ocean) + { + return Convert.ToDouble(groundAlt > 0 ? Math.Min(groundAlt, seaAlt) : seaAlt); + } + else + { + return Convert.ToDouble(groundAlt > 0 ? groundAlt : seaAlt); + } } public override string ToString() diff --git a/src/kOS/Suffixed/VesselTarget.cs b/src/kOS/Suffixed/VesselTarget.cs index c9d28054a..c502bf466 100644 --- a/src/kOS/Suffixed/VesselTarget.cs +++ b/src/kOS/Suffixed/VesselTarget.cs @@ -4,6 +4,7 @@ using kOS.Safe.Encapsulation; using kOS.Safe.Encapsulation.Suffixes; using kOS.Safe.Exceptions; +using kOS.Safe.Execution; using kOS.Safe.Serialization; using kOS.Safe.Utilities; using kOS.Suffixed.Part; @@ -237,6 +238,7 @@ private void InitializeSuffixes() AddSuffix("MAXTHRUST", new Suffix(() => VesselUtils.GetMaxThrust(Vessel))); AddSuffix("MAXTHRUSTAT", new OneArgsSuffix(GetMaxThrustAt)); AddSuffix("FACING", new Suffix(() => VesselUtils.GetFacing(Vessel))); + AddSuffix("BOUNDS", new Suffix(() => GetBoundsValue())); AddSuffix("ANGULARMOMENTUM", new Suffix(() => new Vector(Vessel.angularMomentum))); AddSuffix("ANGULARVEL", new Suffix(() => RawAngularVelFromRelative(Vessel.angularVelocity))); AddSuffix("MASS", new Suffix(() => Vessel.GetTotalMass())); @@ -303,6 +305,42 @@ public MessageQueueStructure GetMessages() return InterVesselManager.Instance.GetQueue(Shared.Vessel, Shared); } + public BoundsValue GetBoundsValue() + { + Direction myFacing = VesselUtils.GetFacing(Vessel); + Quaternion inverseMyFacing = myFacing.Rotation.Inverse(); + Vector rootOrigin = Parts[0].GetPosition(); + Bounds unionBounds = new Bounds(); + for (int pNum = 0; pNum < Parts.Count; ++pNum) + { + PartValue p = Parts[pNum]; + Vector partOriginOffsetInVesselBounds = p.GetPosition() - rootOrigin; + Bounds b = p.GetBoundsValue().GetUnityBounds(); + Vector partCenter = new Vector(b.center); + + // Just like the logic for the part needing all 8 corners of the mesh's bounds, + // this needs all 8 corners of the part bounds: + for (int signX = -1; signX <= 1; signX += 2) + for (int signY = -1; signY <= 1; signY += 2) + for (int signZ = -1; signZ <= 1; signZ += 2) + { + Vector corner = partCenter + new Vector(signX * b.extents.x, signY * b.extents.y, signZ * b.extents.z); + Vector worldCorner = partOriginOffsetInVesselBounds + p.GetFacing() * corner; + Vector3 vesselCorner = inverseMyFacing * worldCorner.ToVector3(); + + unionBounds.Encapsulate(vesselCorner); + } + } + + Vector min = new Vector(unionBounds.min); + Vector max = new Vector(unionBounds.max); + + // The above operation is expensive and should force the CPU to do a WAIT 0: + Shared.Cpu.YieldProgram(new YieldFinishedNextTick()); + + return new BoundsValue(min, max, delegate { return Parts[0].GetPosition(); }, delegate { return VesselUtils.GetFacing(Vessel); }, Shared); + } + public void ThrowIfNotCPUVessel() { if (this.Vessel.id != Shared.Vessel.id) @@ -400,7 +438,7 @@ private void StartTracking() } } - public override ISuffixResult GetSuffix(string suffixName) + public override ISuffixResult GetSuffix(string suffixName, bool failOkay = false) { // Most suffixes are handled by the newer AddSuffix system, except for the // resource levels, which have to use this older technique as a fallback because @@ -413,7 +451,7 @@ public override ISuffixResult GetSuffix(string suffixName) return new SuffixResult(ScalarValue.Create(dblValue)); } - return base.GetSuffix(suffixName); + return base.GetSuffix(suffixName, failOkay); } protected bool Equals(VesselTarget other) diff --git a/src/kOS/Utilities/PartUtilities.cs b/src/kOS/Utilities/PartUtilities.cs index 0105a1b5d..84b6e035e 100644 --- a/src/kOS/Utilities/PartUtilities.cs +++ b/src/kOS/Utilities/PartUtilities.cs @@ -1,4 +1,5 @@ -using kOS.Safe.Encapsulation; +using kOS.Safe.Encapsulation; +using UnityEngine; using System; namespace kOS.Utilities @@ -48,5 +49,6 @@ public static float GetWetMass(this Part part) return mass; } + } } \ No newline at end of file diff --git a/src/kOS/kOS.csproj b/src/kOS/kOS.csproj index 1eb2a86d6..8469a6137 100644 --- a/src/kOS/kOS.csproj +++ b/src/kOS/kOS.csproj @@ -122,6 +122,7 @@ +