diff --git a/Resources/GameData/kOS/Parts/KOSCherryLight/part.cfg b/Resources/GameData/kOS/Parts/KOSCherryLight/part.cfg index 8c7e253a4a..4e7a96b18f 100644 --- a/Resources/GameData/kOS/Parts/KOSCherryLight/part.cfg +++ b/Resources/GameData/kOS/Parts/KOSCherryLight/part.cfg @@ -41,5 +41,10 @@ PART resourceAmount = 0.02 animationName = Rotation } + MODULE + { + name = ModuleCargoPart + packedVolume = 100 + } } diff --git a/Resources/GameData/kOS/Parts/kOSMachine0m/part.cfg b/Resources/GameData/kOS/Parts/kOSMachine0m/part.cfg index 96c94bdcdc..d56bd8bb4c 100755 --- a/Resources/GameData/kOS/Parts/kOSMachine0m/part.cfg +++ b/Resources/GameData/kOS/Parts/kOSMachine0m/part.cfg @@ -1,76 +1,81 @@ PART { -// --- general parameters --- -name = KR-2042 -module = Part -author = SMA and Peter Goddard + // --- general parameters --- + name = KR-2042 + module = Part + author = SMA and Peter Goddard -// --- asset parameters --- -mesh = model/model.mu -scale = 1 -rescaleFactor = 1 + // --- asset parameters --- + mesh = model/model.mu + scale = 1 + rescaleFactor = 1 -// --- node definitions --- -// definition format is Position X, Position Y, Position Z, Up X, Up Y, Up Z, connector node size -node_stack_bottom = 0.0, -0.081, 0.0, 0.0, -1.0, 0.0, 0 -node_stack_top = 0.0, -0.003, 0.0, 0.0, 1.0, 0.0, 0 + // --- node definitions --- + // definition format is Position X, Position Y, Position Z, Up X, Up Y, Up Z, connector node size + node_stack_bottom = 0.0, -0.081, 0.0, 0.0, -1.0, 0.0, 0 + node_stack_top = 0.0, -0.003, 0.0, 0.0, 1.0, 0.0, 0 -// --- Tech tree --- -TechRequired = precisionEngineering + // --- Tech tree --- + TechRequired = precisionEngineering -// --- editor parameters --- -cost = 1200 -entryCost = 6800 -category = Control -subcategory = 0 -title = KR-2042 b Scriptable Control System -manufacturer = Compotronix -description = Would you trust life and limb to a mindless autopilot, powered by untested software you hastily wrote yourself? Spacefaring kerbals would! -bulkheadProfiles = size0 + // --- editor parameters --- + cost = 1200 + entryCost = 6800 + category = Control + subcategory = 0 + title = KR-2042 b Scriptable Control System + manufacturer = Compotronix + description = Would you trust life and limb to a mindless autopilot, powered by untested software you hastily wrote yourself? Spacefaring kerbals would! + bulkheadProfiles = size0 -// attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision -attachRules = 1,0,1,0,0 + // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision + attachRules = 1,0,1,0,0 -// --- standard part parameters --- -mass = 0.08 -dragModelType = default -maximum_drag = 0.2 -minimum_drag = 0.2 -angularDrag = 2 -crashTolerance = 9 -maxTemp = 1500 + // --- standard part parameters --- + mass = 0.08 + dragModelType = default + maximum_drag = 0.2 + minimum_drag = 0.2 + angularDrag = 2 + crashTolerance = 9 + maxTemp = 1500 -MODULE -{ - name = kOSProcessor - diskSpace = 5000 - ECPerBytePerSecond = 0 - ECPerInstruction = 0.000004 -} + MODULE + { + name = kOSProcessor + diskSpace = 5000 + ECPerBytePerSecond = 0 + ECPerInstruction = 0.000004 + } -RESOURCE -{ - name = ElectricCharge - amount = 5 - maxAmount = 5 -} + RESOURCE + { + name = ElectricCharge + amount = 5 + maxAmount = 5 + } -MODULE -{ - name = ModuleLight - lightName = PowerLight - lightR = 0 - lightG = 0.1 - lightB = 0 -} + MODULE + { + name = ModuleLight + lightName = PowerLight + lightR = 0 + lightG = 0.1 + lightB = 0 + } -MODULE -{ - name = kOSLightModule - resourceAmount = 0.02 - animationName = flickerStart -} + MODULE + { + name = kOSLightModule + resourceAmount = 0.02 + animationName = flickerStart + } + MODULE + { + name = ModuleCargoPart + packedVolume = 60 + } } diff --git a/Resources/GameData/kOS/Parts/kOSMachine0mLegacy/part.cfg b/Resources/GameData/kOS/Parts/kOSMachine0mLegacy/part.cfg index d0cc0b84e7..14993d36d0 100644 --- a/Resources/GameData/kOS/Parts/kOSMachine0mLegacy/part.cfg +++ b/Resources/GameData/kOS/Parts/kOSMachine0mLegacy/part.cfg @@ -24,6 +24,7 @@ PART // --- Tech tree --- TechRequired = precisionEngineering + TechHidden = true // --- editor parameters --- cost = 1200 diff --git a/Resources/GameData/kOS/Parts/kOSMachine1m/part.cfg b/Resources/GameData/kOS/Parts/kOSMachine1m/part.cfg index e604219d11..005390bf64 100755 --- a/Resources/GameData/kOS/Parts/kOSMachine1m/part.cfg +++ b/Resources/GameData/kOS/Parts/kOSMachine1m/part.cfg @@ -1,58 +1,64 @@ PART { -// --- general parameters --- V2~ fixed collision mesh -name = kOSMachine1m -module = Part -author = KevinLaity / Peter Goddard - -// --- asset parameters --- -mesh = model/model.mu -scale = 1 -rescaleFactor = 0.99999999999 -iconCenter = 0, 3, 0 - -// --- node definitions --- -node_attach = 0.0, 0.0, 0.0, 0.0, -1.0, 0.0 -node_stack_bottom = 0.0, -0.173, 0.0, 0.0, -1.0, 0.0 -node_stack_top = 0.0, 0.173, 0.0, 0.0, 1.0, 0.0 - -// --- Tech tree --- -TechRequired = flightControl - -// --- editor parameters --- -cost = 1200 -entryCost = 4200 -category = Control -subcategory = 0 -title = CX-4181 Scriptable Control System -manufacturer = Compotronix -description = Would you trust life and limb to a mindless autopilot, powered by untested software you hastily wrote yourself? Spacefaring kerbals would! -bulkheadProfiles = size1 - -// attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision -attachRules = 1,0,1,1,0 - -// --- standard part parameters --- -mass = 0.12 -dragModelType = default -maximum_drag = 0.2 -minimum_drag = 0.2 -angularDrag = 2 -crashTolerance = 9 -maxTemp = 2000 - -MODULE -{ - name = kOSProcessor - diskSpace = 10000 - ECPerBytePerSecond = 0 - ECPerInstruction = 0.000004 -} + // --- general parameters --- V2~ fixed collision mesh + name = kOSMachine1m + module = Part + author = KevinLaity / Peter Goddard -RESOURCE -{ - name = ElectricCharge - amount = 5 - maxAmount = 5 -} + // --- asset parameters --- + mesh = model/model.mu + scale = 1 + rescaleFactor = 0.99999999999 + iconCenter = 0, 3, 0 + + // --- node definitions --- + node_attach = 0.0, 0.0, 0.0, 0.0, -1.0, 0.0 + node_stack_bottom = 0.0, -0.173, 0.0, 0.0, -1.0, 0.0 + node_stack_top = 0.0, 0.173, 0.0, 0.0, 1.0, 0.0 + + // --- Tech tree --- + TechRequired = flightControl + + // --- editor parameters --- + cost = 1200 + entryCost = 4200 + category = Control + subcategory = 0 + title = CX-4181 Scriptable Control System + manufacturer = Compotronix + description = Would you trust life and limb to a mindless autopilot, powered by untested software you hastily wrote yourself? Spacefaring kerbals would! + bulkheadProfiles = size1 + + // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision + attachRules = 1,0,1,1,0 + + // --- standard part parameters --- + mass = 0.12 + dragModelType = default + maximum_drag = 0.2 + minimum_drag = 0.2 + angularDrag = 2 + crashTolerance = 9 + maxTemp = 2000 + + MODULE + { + name = kOSProcessor + diskSpace = 10000 + ECPerBytePerSecond = 0 + ECPerInstruction = 0.000004 + } + + RESOURCE + { + name = ElectricCharge + amount = 5 + maxAmount = 5 + } + + MODULE + { + name = ModuleCargoPart + packedVolume = 650 + } } diff --git a/Resources/GameData/kOS/Parts/kOSMachineRad/part.cfg b/Resources/GameData/kOS/Parts/kOSMachineRad/part.cfg index 60dfb2a800..8525de4aa5 100755 --- a/Resources/GameData/kOS/Parts/kOSMachineRad/part.cfg +++ b/Resources/GameData/kOS/Parts/kOSMachineRad/part.cfg @@ -1,74 +1,77 @@ PART { -// --- general parameters --- -name = kOSMachineRad -module = Part -author = Peter Goddard + // --- general parameters --- + name = kOSMachineRad + module = Part + author = Peter Goddard -// --- asset parameters --- -mesh = model/model.mu -scale = 1 -rescaleFactor = 1 -iconCenter = 0, 0, 0 + // --- asset parameters --- + mesh = model/model.mu + scale = 1 + rescaleFactor = 1 + iconCenter = 0, 0, 0 -// --- node definitions --- -node_attach = 0.0, 0.0, 0.0, 1, 0, 0 + // --- node definitions --- + node_attach = 0.0, 0.0, 0.0, 1, 0, 0 -// --- Tech tree --- -TechRequired = unmannedTech + // --- Tech tree --- + TechRequired = unmannedTech -// --- editor parameters --- -cost = 2200 -entryCost = 4200 -category = Control -subcategory = 0 -title = CompoMax Radial Tubeless -manufacturer = Squalid-State Devices Inc. -description = Would you trust life and limb to a mindless autopilot, powered by untested software you hastily wrote yourself? Spacefaring kerbals would! -bulkheadProfiles = srf + // --- editor parameters --- + cost = 2200 + entryCost = 4200 + category = Control + subcategory = 0 + title = CompoMax Radial Tubeless + manufacturer = Squalid-State Devices Inc. + description = Would you trust life and limb to a mindless autopilot, powered by untested software you hastily wrote yourself? Spacefaring kerbals would! + bulkheadProfiles = srf -// attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision -attachRules = 0,1,0,0,1 + // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision + attachRules = 0,1,0,0,1 -// --- standard part parameters --- -mass = 0.03 -dragModelType = default -maximum_drag = 0.0 -minimum_drag = 0.0 -angularDrag = 0 -crashTolerance = 6 -maxTemp = 1500 + // --- standard part parameters --- + mass = 0.03 + dragModelType = default + maximum_drag = 0.0 + minimum_drag = 0.0 + angularDrag = 0 + crashTolerance = 6 + maxTemp = 1500 - -MODULE -{ - name = kOSProcessor - diskSpace = 60000 - ECPerBytePerSecond = 0 - ECPerInstruction = 0.000004 -} -MODULE -{ - name = ModuleDeployableSolarPanel - sunTracking = false - raycastTransformName = suncatcher - pivotName = suncatcher - isBreakable = false - resourceName = ElectricCharge - chargeRate = 0.5 - powerCurve - { - key = 206000000000 0 0 0 - key = 13599840256 1 0 0 - key = 68773560320 0.5 0 0 - key = 0 10 0 0 - } -} -} -RESOURCE -{ - name = ElectricCharge - amount = 10 - maxAmount = 10 -} + MODULE + { + name = kOSProcessor + diskSpace = 60000 + ECPerBytePerSecond = 0 + ECPerInstruction = 0.000004 + } + RESOURCE + { + name = ElectricCharge + amount = 10 + maxAmount = 10 + } + MODULE + { + name = ModuleCargoPart + packedVolume = 200 + } + MODULE + { + name = ModuleDeployableSolarPanel + sunTracking = false + raycastTransformName = suncatcher + pivotName = suncatcher + isBreakable = false + resourceName = ElectricCharge + chargeRate = 0.5 + powerCurve + { + key = 206000000000 0 0 0 + key = 13599840256 1 0 0 + key = 68773560320 0.5 0 0 + key = 0 10 0 0 + } + } } diff --git a/Resources/GameData/kOS/Parts/kOSkal9000/part.cfg b/Resources/GameData/kOS/Parts/kOSkal9000/part.cfg index 5aeacb6aab..0238390084 100644 --- a/Resources/GameData/kOS/Parts/kOSkal9000/part.cfg +++ b/Resources/GameData/kOS/Parts/kOSkal9000/part.cfg @@ -1,61 +1,66 @@ PART { -// --- general parameters --- -name = KAL9000 -module = Part -author = Peter Goddard and kOS Crew - -// --- asset parameters --- -mesh = model/model.mu -scale = 1 -rescaleFactor = 1 - -// --- node definitions --- -node_attach = 0.01, 0.0, 0.0, 1, 0, 0, 0 - -// --- Tech tree --- -TechRequired = automation - -// --- editor parameters --- -cost = 1200 -entryCost = 6800 -category = Control -subcategory = 0 -title = KAL9000 Scriptable Control System -manufacturer = Squalid State Devices -description = Mildly Malevolent artificial entity, use caution on EVA's -bulkheadProfiles = srf - -// attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision -attachRules = 0,1,0,0,0 - -// --- standard part parameters --- -mass = 0.0005 -dragModelType = default -maximum_drag = 0 -minimum_drag = 0 -angularDrag = 0 -crashTolerance = 9 -maxTemp = 1500 - -MODULE -{ - name = kOSProcessor - diskSpace = 255000 -} -MODULE -{ - name = ModuleLight - lightName = PowerLight - lightR = 0.5 - lightG = 0 - lightB = 0 -} + // --- general parameters --- + name = KAL9000 + module = Part + author = Peter Goddard and kOS Crew -MODULE -{ - name = kOSLightModule - resourceAmount = 0.02 - animationName = KAL9000Lives -} + // --- asset parameters --- + mesh = model/model.mu + scale = 1 + rescaleFactor = 1 + + // --- node definitions --- + node_attach = 0.01, 0.0, 0.0, 1, 0, 0, 0 + + // --- Tech tree --- + TechRequired = automation + + // --- editor parameters --- + cost = 1200 + entryCost = 6800 + category = Control + subcategory = 0 + title = KAL9000 Scriptable Control System + manufacturer = Squalid State Devices + description = Mildly Malevolent artificial entity, use caution on EVA's + bulkheadProfiles = srf + + // attachment rules: stack, srfAttach, allowStack, allowSrfAttach, allowCollision + attachRules = 0,1,0,0,0 + + // --- standard part parameters --- + mass = 0.0005 + dragModelType = default + maximum_drag = 0 + minimum_drag = 0 + angularDrag = 0 + crashTolerance = 9 + maxTemp = 1500 + + MODULE + { + name = kOSProcessor + diskSpace = 255000 + } + MODULE + { + name = ModuleLight + lightName = PowerLight + lightR = 0.5 + lightG = 0 + lightB = 0 + } + + MODULE + { + name = kOSLightModule + resourceAmount = 0.02 + animationName = KAL9000Lives + } + MODULE + { + name = ModuleCargoPart + packedVolume = 50 + } } diff --git a/doc/source/bindings.rst b/doc/source/bindings.rst index fb4c5f7e3a..9aab7068c5 100644 --- a/doc/source/bindings.rst +++ b/doc/source/bindings.rst @@ -532,6 +532,35 @@ for this purpose. For Kerbals, it refers to a more arbitrary line in space, pointing at a fixed point in the firmament, also known as the "skybox". +OPCODESLEFT +--------- + +This returns the amount of IPU that are left in this physics tick. This means +that if you receive the value 20, you can run 20 more instructions. After this +amount of instructions, other CPUs will run their instructions and then +`TIME:SECONDS` will increase. + +OPCODESLEFT can be used to try to make sure you run a block of code in one +physics tick. This is useful when working with vectors or when interacting +with shared message queues. + +To use: + + // Will always wait the first time, becomes more accurate the second time. + GLOBAL OPCODESNEEDED TO 1000. + IF OPCODESLEFT < OPCODESNEEDED + WAIT 0. + LOCAL STARTIPU TO OPCODESLEFT. + LOCAL STARTTIME TO TIME:SECONDS. + + // your code here, make sure to keep the instruction count lower than your CONFIG:IPU + + IF STARTTIME = TIME:SECONDS { + SET OPCODESNEEDED TO STARTIPU - OPCODESLEFT. + } ELSE { + PRINT "Code is taking too long to execute. Please make the code shorter or raise the IPU.". + } + Addons ------ diff --git a/doc/source/structures/celestial_bodies/body.rst b/doc/source/structures/celestial_bodies/body.rst index e8a4f3cfea..9f23c559c6 100644 --- a/doc/source/structures/celestial_bodies/body.rst +++ b/doc/source/structures/celestial_bodies/body.rst @@ -170,6 +170,7 @@ All of the main celestial bodies in the game are reserved variable names. The fo .. method:: Body:GEOPOSITIONOF(vectorPos) :parameter vectorPos: :struct:`Vector` input position in XYZ space. + :type: :struct:`GeoCoordinates` The geoposition underneath the given vector position. SHIP:BODY:GEOPOSITIONOF(SHIP:POSITION) should, in principle, give the same thing as SHIP:GEOPOSITION, while SHIP:BODY:GEOPOSITIONOF(SHIP:POSITION + 1000*SHIP:NORTH) would give you the lat/lng of the position 1 kilometer north of you. Be careful not to confuse this with :GEOPOSITION (no "OF" in the name), which is also a suffix of Body by virtue of the fact that Body is an Orbitable, but it doesn't mean the same thing. diff --git a/doc/source/structures/misc/vecdraw.rst b/doc/source/structures/misc/vecdraw.rst index d7178fa0b8..9c034e156e 100644 --- a/doc/source/structures/misc/vecdraw.rst +++ b/doc/source/structures/misc/vecdraw.rst @@ -8,13 +8,19 @@ 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, pointy) -.. function:: VECDRAWARGS(start, vec, color, label, scale, show, width, pointy) +.. function:: VECDRAW(start, vec, color, label, scale, show, width, pointy, wiping) +.. function:: VECDRAWARGS(start, vec, color, label, scale, show, width, pointy, wiping) Both these two function names do the same thing. For historical reasons both names exist, but now they both do the same thing. They create a new ``vecdraw`` object that you can then manipulate - to show things on the screen:: + to show things on the screen. + + For an explanation what the parameters start, vec, color, label, scale, show, + width, pointy, and wiping mean, they correspond to the same suffix names + below in the table. + + Here are some examples:: SET anArrow TO VECDRAW( V(0,0,0), diff --git a/doc/source/structures/orbits/eta.rst b/doc/source/structures/orbits/eta.rst index 335660bb57..446b7666a5 100644 --- a/doc/source/structures/orbits/eta.rst +++ b/doc/source/structures/orbits/eta.rst @@ -22,31 +22,32 @@ on an Orbit, and can be obtained one of two ways: print SHIP:OBT:ETA:APOAPSIS - .. structure:: OrbitEta + .. structure:: OrbitEta - .. list-table:: - :header-rows: 1 - :widths: 2 1 4 + .. list-table:: - * - Suffix - - Type - - Description + :header-rows: 1 + :widths: 2 1 4 - * - :attr:`APOAPSIS` - - :ref:`scalar `, seconds - - Seconds from now until apoapsis. + * - Suffix + - Type + - Description - * - :attr:`PERIAPSIS` - - :ref:`scalar `, seconds - - Seconds from now until periapsis. + * - :attr:`APOAPSIS` + - :ref:`scalar `, seconds + - Seconds from now until apoapsis. - * - :attr:`NEXTNODE` - - :ref:`scalar `, seconds - - Seconds from now until the next maneuver node. + * - :attr:`PERIAPSIS` + - :ref:`scalar `, seconds + - Seconds from now until periapsis. - * - :attr:`TRANSITION` - - :ref:`scalar `, seconds - - Seconds from now until the next orbit patch starts. + * - :attr:`NEXTNODE` + - :ref:`scalar `, seconds + - Seconds from now until the next maneuver node. + + * - :attr:`TRANSITION` + - :ref:`scalar `, seconds + - Seconds from now until the next orbit patch starts. .. attribute:: ETA:APOAPSIS diff --git a/src/kOS.Safe/Binding/BoundVariable.cs b/src/kOS.Safe/Binding/BoundVariable.cs index 17ceab46c0..8c42e28afe 100644 --- a/src/kOS.Safe/Binding/BoundVariable.cs +++ b/src/kOS.Safe/Binding/BoundVariable.cs @@ -1,4 +1,4 @@ -using kOS.Safe.Execution; +using kOS.Safe.Execution; using kOS.Safe.Encapsulation; namespace kOS.Safe.Binding @@ -8,6 +8,8 @@ public class BoundVariable : Variable public BindingSetDlg Set; public BindingGetDlg Get; + public bool Volatile = false; + private object currentValue; public override object Value @@ -20,7 +22,10 @@ public override object Value // new primitive encapsulation types we instead encapsulate any value returned // by the delegate. This makes it so that all of the getters for bound variables // don't need to be modified to explicitly return the encapsulated types. - return currentValue ?? (currentValue = Structure.FromPrimitive(Get())); + if (!Volatile && currentValue != null) + return currentValue; + currentValue = Structure.FromPrimitive(Get()); + return currentValue; } set { diff --git a/src/kOS.Safe/Binding/IBindingManager.cs b/src/kOS.Safe/Binding/IBindingManager.cs index de4e48fd3f..01a03182f0 100644 --- a/src/kOS.Safe/Binding/IBindingManager.cs +++ b/src/kOS.Safe/Binding/IBindingManager.cs @@ -11,6 +11,7 @@ public interface IBindingManager void AddSetter(string name, BindingSetDlg dlg); void AddSetter(IEnumerable names, BindingSetDlg dlg); bool HasGetter(string name); + void MarkVolatile(string name); void PreUpdate(); void PostUpdate(); void ToggleFlyByWire(string paramName, bool enabled); diff --git a/src/kOS.Safe/Exceptions/KOSYouShouldNeverSeeThisException.cs b/src/kOS.Safe/Exceptions/KOSYouShouldNeverSeeThisException.cs index 9bd0113d16..63c7bed055 100644 --- a/src/kOS.Safe/Exceptions/KOSYouShouldNeverSeeThisException.cs +++ b/src/kOS.Safe/Exceptions/KOSYouShouldNeverSeeThisException.cs @@ -5,7 +5,7 @@ namespace kOS.Safe.Exceptions { public class KOSYouShouldNeverSeeThisException : KOSException { - public KOSYouShouldNeverSeeThisException(string message) : base("This is an error endusers should never see, if you see this please report it to the kOS devs:\r\n " + message) + public KOSYouShouldNeverSeeThisException(string message) : base("This is an error end users should never see. If you see this please report it on the kOS github:\r\n " + message) { } } diff --git a/src/kOS.Safe/Execution/CPU.cs b/src/kOS.Safe/Execution/CPU.cs index 08534b28bd..d2c8751fba 100644 --- a/src/kOS.Safe/Execution/CPU.cs +++ b/src/kOS.Safe/Execution/CPU.cs @@ -507,7 +507,8 @@ public void BreakExecution(bool manual) { PopFirstContext(); shared.Screen.Print("Program aborted."); - shared.SoundMaker.StopAllVoices(); // stop voices if execution was manually broken, but not if the program ends normally + if (shared.SoundMaker != null) + shared.SoundMaker.StopAllVoices(); // stop voices if execution was manually broken, but not if the program ends normally PrintStatistics(); stack.Clear(); } @@ -1502,8 +1503,8 @@ private void ContinueExecution(bool doProfiling) } else { - executeNext = ExecuteInstruction(currentContext, doProfiling); ++InstructionsThisUpdate; + executeNext = ExecuteInstruction(currentContext, doProfiling); if (CurrentPriority == InterruptPriority.Normal) ++howManyNormalPriority; } diff --git a/src/kOS.Standalone/Bindings/PolyfillBindings.cs b/src/kOS.Standalone/Bindings/PolyfillBindings.cs new file mode 100644 index 0000000000..26aa3d2301 --- /dev/null +++ b/src/kOS.Standalone/Bindings/PolyfillBindings.cs @@ -0,0 +1,27 @@ +using kOS.Safe.Binding; +using kOS.Safe; +using kOS.Safe.Encapsulation; +using kOS.Standalone; +using kOS.Standalone.Suffixed; + +namespace kOS.Binding +{ + [Binding("ksp")] + public class PolyfillBindings : SafeBindingBase + { + private TerminalStruct terminal; + + public override void AddTo(SafeSharedObjects shared) + { + terminal = new TerminalStruct(shared); + + shared.BindingMgr.AddGetter("TERMINAL", delegate { return terminal; }); + if (shared is StandaloneSharedObjects) + { + shared.BindingMgr.AddGetter("TIME", delegate { return new TimePolyfill(((StandaloneSharedObjects)shared).StartTime); }); + shared.BindingMgr.AddGetter("SHIP", delegate { return ((StandaloneSharedObjects)shared).StandaloneShip; }); + shared.BindingMgr.AddGetter("CORE", delegate { return ((StandaloneSharedObjects)shared).StandaloneCore; }); + } + } + } +} diff --git a/src/kOS.Standalone/DebugLogger.cs b/src/kOS.Standalone/DebugLogger.cs new file mode 100644 index 0000000000..d4714fde77 --- /dev/null +++ b/src/kOS.Standalone/DebugLogger.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Text; +using kOS.Safe; +using System.Diagnostics; + +namespace kOS.Standalone +{ + class DebugLogger : ILogger + { + public void Log(string text) + { + Debug.WriteLine(text); + } + + public void Log(Exception e) + { + Debug.WriteLine(e); + } + + public void LogError(string s) + { + Debug.WriteLine(s); + } + + public void LogException(Exception exception) + { + Debug.WriteLine(exception); + } + + public void LogWarning(string s) + { + Debug.WriteLine(s); + } + + public void LogWarningAndScreen(string s) + { + Debug.WriteLine(s); + } + + public void SuperVerbose(string s) + { + Debug.WriteLine(s); + } + } +} diff --git a/src/kOS.Standalone/Program.cs b/src/kOS.Standalone/Program.cs new file mode 100644 index 0000000000..2cded1d033 --- /dev/null +++ b/src/kOS.Standalone/Program.cs @@ -0,0 +1,174 @@ +using System; +using System.Text; +using kOS.Safe.Screen; +using kOS.Safe.UserIO; +using kOS.Safe.Utilities; +using kOS.Safe.Compilation; +using kOS.Safe.Serialization; + +namespace kOS.Standalone +{ + class Program + { + static void Main(string[] args) + { + kOS.Safe.Utilities.SafeHouse.Init( + new StandaloneConfig(), + new Safe.Encapsulation.VersionInfo(0, 0, 0, 0), + "http://ksp-kos.github.io/KOS_DOC/", + Environment.NewLine == "\r\n", + "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Kerbal Space Program\\Ships\\Script" + ); + kOS.Safe.Utilities.SafeHouse.Logger = new DebugLogger(); + AssemblyWalkAttribute.Walk(); + Opcode.InitMachineCodeData(); + CompiledObject.InitTypeData(); + SafeSerializationMgr.CheckIDumperStatics(); + + Console.TreatControlCAsInput = true; + + while (true) + { + Console.Clear(); + int lineOffset = 0; + + var shared = new StandaloneSharedObjects(); + kOS.Safe.Utilities.SafeHouse.Logger = shared.Logger; + + shared.VolumeMgr.SwitchTo(shared.VolumeMgr.GetVolume(0)); + + shared.Screen.SetSize(Console.WindowHeight, Console.BufferWidth); + IScreenSnapShot snapshot = ScreenSnapShot.EmptyScreen(shared.Screen); + + shared.Cpu.Boot(); + + while (shared.ProcessorMode == Safe.Module.ProcessorModes.READY) + { + if (shared.Screen.RowCount != Console.WindowHeight || shared.Screen.ColumnCount != Console.BufferWidth) + shared.Screen.SetSize(Console.WindowHeight, Console.BufferWidth); + + Console.CursorVisible = shared.Interpreter.IsWaitingForCommand(); + if (Console.KeyAvailable) + { + var key = Console.ReadKey(true); + char mapped = key.KeyChar; + bool special = true; + switch (mapped) { + case '\u0003': + mapped = (char)UnicodeCommand.BREAK; + break; + default: + special = false; + break; + } + switch (key.Key) { + case ConsoleKey.PageUp: + special = true; + mapped = (char)UnicodeCommand.PAGEUPCURSOR; + break; + case ConsoleKey.PageDown: + special = true; + mapped = (char)UnicodeCommand.PAGEDOWNCURSOR; + break; + case ConsoleKey.UpArrow: + special = true; + mapped = (char)UnicodeCommand.UPCURSORONE; + break; + case ConsoleKey.DownArrow: + special = true; + mapped = (char)UnicodeCommand.DOWNCURSORONE; + break; + case ConsoleKey.LeftArrow: + special = true; + mapped = (char)UnicodeCommand.LEFTCURSORONE; + break; + case ConsoleKey.RightArrow: + special = true; + mapped = (char)UnicodeCommand.RIGHTCURSORONE; + break; + case ConsoleKey.Home: + special = true; + mapped = (char)UnicodeCommand.HOMECURSOR; + break; + case ConsoleKey.End: + special = true; + mapped = (char)UnicodeCommand.ENDCURSOR; + break; + } + + if (shared.Interpreter.IsWaitingForCommand() || mapped == (char)UnicodeCommand.BREAK) { + if (special) + shared.Interpreter.SpecialKey(mapped); + else + shared.Interpreter.Type(mapped); + } else { + shared.Screen.CharInputQueue.Enqueue(mapped); + } + } + + var oldSnapshot = snapshot; + snapshot = new ScreenSnapShot(shared.Screen).DeepCopy(); + var diff = snapshot.DiffFrom(oldSnapshot); + + int i = 0; + while (i < diff.Length) + { + char c = diff[i]; + + switch (c) + { + case (char)UnicodeCommand.CLEARSCREEN: + Console.Clear(); + i++; + break; + case (char)UnicodeCommand.TITLEBEGIN: + i++; + var titleBuilder = new StringBuilder(); + + while (diff[i] != (char)UnicodeCommand.TITLEEND) + { + titleBuilder.Append(diff[i]); + i++; + } + Console.Title = titleBuilder.ToString(); + i++; + break; + case (char)UnicodeCommand.TELEPORTCURSOR: + i++; + Console.CursorLeft = (int)diff[i++]; + Console.CursorTop = (int)diff[i++] + lineOffset; + break; + case (char)UnicodeCommand.BEEP: + i++; + Console.Beep(); + break; + case (char)UnicodeCommand.SCROLLSCREENUPONE: + if (Console.CursorTop + 1 == Console.BufferHeight) + Console.BufferHeight *= 2; + Console.CursorTop++; + lineOffset++; + i++; + break; + case (char)UnicodeCommand.SCROLLSCREENDOWNONE: + Console.CursorTop--; + lineOffset--; + i++; + break; + default: + if (c > 255 && c != 0x2588) + System.Diagnostics.Debug.WriteLine("Unknown: " + c); + Console.Write(c); + i++; + break; + } + } + + shared.UpdateHandler.UpdateObservers(0.05); + shared.UpdateHandler.UpdateFixedObservers(0.05); + } + if (shared.ProcessorMode == Safe.Module.ProcessorModes.OFF) + break; + } + } + } +} diff --git a/src/kOS.Standalone/StandaloneBindingManager.cs b/src/kOS.Standalone/StandaloneBindingManager.cs new file mode 100644 index 0000000000..2ce2f0f81a --- /dev/null +++ b/src/kOS.Standalone/StandaloneBindingManager.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Linq; +using kOS.Safe.Binding; +using kOS.Safe.Utilities; + +namespace kOS.Standalone +{ + [AssemblyWalk(AttributeType = typeof(BindingAttribute), InherritedType = typeof(SafeBindingBase), StaticRegisterMethod = "RegisterMethod")] + class StandaloneBindingManager : IBindingManager + { + private readonly StandaloneSharedObjects shared; + private readonly List bindings = new List(); + private readonly Dictionary variables; + + // Note: When we were using .Net 3.5, This used to be a Dictionary rather than a HashSet of pairs. But that had to + // change because of one .Net 4.x change to how reflection on Attributes works. In .Net 3.5, an Attribute called + // [Foo(1,2)] attached to classA was considered un-equal to an attribute with the same values ([Foo(1,2)]) attached + // to classB. But in .Net 4.0, which class the attribute is attached to is no longer part of its equality test, + // therefore both those examples would be "equal" classes because they are the same name Foo with the same paramters (1,2). + // This meant that when we had many classes decorated with exactly the same thing, [Binding("ksp")], these Attributes + // could be unique keys in a Dictionary in .Net 3.5 because they weren't attached to the same class, but in .Net 4.0 + // they became key clashes because they were now considered "equal" and all such Attributes after the first were + // refusing to be stored in the dictionary. + private static readonly HashSet> rawAttributes = new HashSet>(); + + public StandaloneBindingManager(StandaloneSharedObjects shared) + { + variables = new Dictionary(StringComparer.OrdinalIgnoreCase); + this.shared = shared; + this.shared.BindingMgr = this; + } + + public void Load() + { + var contexts = new string[1]; + contexts[0] = "ksp"; + + bindings.Clear(); + variables.Clear(); + + foreach (KeyValuePair attrTypePair in rawAttributes) + { + var type = attrTypePair.Value; + if (attrTypePair.Key.Contexts.Any() && !attrTypePair.Key.Contexts.Intersect(contexts).Any()) continue; + var instanceWithABinding = (SafeBindingBase)Activator.CreateInstance(type); + instanceWithABinding.AddTo(shared); + bindings.Add(instanceWithABinding); + } + } + + public static void RegisterMethod(BindingAttribute attr, Type type) + { + KeyValuePair attrTypePair = new KeyValuePair(attr, type); + if (attr != null && !rawAttributes.Contains(attrTypePair)) + { + rawAttributes.Add(attrTypePair); + } + } + + public void AddBoundVariable(string name, BindingGetDlg getDelegate, BindingSetDlg setDelegate) + { + BoundVariable variable; + if (variables.ContainsKey(name)) + { + variable = variables[name]; + } + else + { + variable = new BoundVariable + { + Name = name, + }; + variables.Add(name, variable); + shared.Cpu.AddVariable(variable, name, false); + } + + if (getDelegate != null) + variable.Get = getDelegate; + + if (setDelegate != null) + variable.Set = setDelegate; + } + + public void AddGetter(string name, BindingGetDlg dlg) + { + AddBoundVariable(name, dlg, null); + } + + public void AddGetter(IEnumerable names, BindingGetDlg dlg) + { + foreach (var name in names) + { + AddBoundVariable(name, dlg, null); + } + } + + public void AddSetter(string name, BindingSetDlg dlg) + { + AddBoundVariable(name, null, dlg); + } + + public void AddSetter(IEnumerable names, BindingSetDlg dlg) + { + foreach (var name in names) + { + AddBoundVariable(name, null, dlg); + } + } + + public bool HasGetter(string name) + { + return variables.ContainsKey(name); + } + + /// + /// Indicates that the binding should not be cached during execution + /// + /// The binding to modify + public void MarkVolatile(string name) + { + variables[name].Volatile = true; + } + + public void PreUpdate() + { + foreach (var variable in variables) + { + variable.Value.ClearCache(); + } + // update the bindings + foreach (var b in bindings) + { + b.Update(); + } + } + + public void PostUpdate() + { + } + + public void ToggleFlyByWire(string paramName, bool enabled) { } + + public void SelectAutopilotMode(string autopilotMode) { } + } +} diff --git a/src/kOS.Standalone/StandaloneConfig.cs b/src/kOS.Standalone/StandaloneConfig.cs new file mode 100644 index 0000000000..957dec899a --- /dev/null +++ b/src/kOS.Standalone/StandaloneConfig.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Text; +using kOS.Safe.Encapsulation; +using kOS.Safe.Encapsulation.Suffixes; + +namespace kOS.Standalone +{ + class StandaloneConfig : IConfig + { + public StandaloneConfig() + { + InstructionsPerUpdate = 20000; + VerboseExceptions = true; + } + + public int InstructionsPerUpdate { get; set; } + public bool UseCompressedPersistence { get; set; } + public bool ShowStatistics { get; set; } + public bool StartOnArchive { get => true; set { } } + public bool ObeyHideUI { get; set; } + public bool EnableSafeMode { get; set; } + public bool VerboseExceptions { get; set; } + public bool EnableTelnet { get; set; } + public int TelnetPort { get; set; } + public bool AudibleExceptions { get; set; } + public string TelnetIPAddrString { get; set; } + public bool UseBlizzyToolbarOnly { get; set; } + public int TerminalFontDefaultSize { get; set; } + public string TerminalFontName { get; set; } + public double TerminalBrightness { get; set; } + public int TerminalDefaultWidth { get; set; } + public int TerminalDefaultHeight { get; set; } + public bool SuppressAutopilot { get; set; } + + public DateTime TimeStamp => throw new NotImplementedException(); + + public bool DebugEachOpcode { get; set; } + + public IList GetConfigKeys() + { + throw new NotImplementedException(); + } + + public ISuffixResult GetSuffix(string suffixName, bool failOkay = false) + { + throw new NotImplementedException(); + } + + public void SaveConfig() + { + throw new NotImplementedException(); + } + + public bool SetSuffix(string suffixName, object value, bool failOkay = false) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/kOS.Standalone/StandaloneEventDispatchManager.cs b/src/kOS.Standalone/StandaloneEventDispatchManager.cs new file mode 100644 index 0000000000..31d751035c --- /dev/null +++ b/src/kOS.Standalone/StandaloneEventDispatchManager.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Text; +using kOS.Safe.Callback; +using kOS.Safe.Execution; + +namespace kOS.Standalone +{ + class StandaloneEventDispatchManager : IGameEventDispatchManager + { + public void Clear() + { + } + + public void RemoveDispatcherFor(ProgramContext context) + { + } + + public void SetDispatcherFor(ProgramContext context) + { + } + } +} diff --git a/src/kOS.Standalone/StandaloneInterpreter.cs b/src/kOS.Standalone/StandaloneInterpreter.cs new file mode 100644 index 0000000000..950db65248 --- /dev/null +++ b/src/kOS.Standalone/StandaloneInterpreter.cs @@ -0,0 +1,207 @@ +using System; +using System.Collections.Generic; +using System.Text; +using kOS.Safe.Screen; +using kOS.Safe.Execution; +using kOS.Safe.UserIO; +using kOS.Safe.Compilation; + +namespace kOS.Standalone +{ + class StandaloneInterpreter : TextEditor, IInterpreter + { + public const string InterpreterName = "standalone"; + private readonly List commandHistory = new List(); + private int commandHistoryIndex; + private bool locked; + + + protected StandaloneSharedObjects Shared { get; private set; } + + public StandaloneInterpreter(StandaloneSharedObjects shared) + { + Shared = shared; + } + + protected override void NewLine() + { + string commandText = LineBuilder.ToString(); + + if (Shared.ScriptHandler.IsCommandComplete(commandText)) + { + base.NewLine(); + AddCommandHistoryEntry(commandText); // add to history first so that if ProcessCommand generates an exception, + // the command is present in the history to be found and printed in the + // error message. + ProcessCommand(commandText); + int numRows = LineSubBuffer.RowCount; + LineSubBuffer.Wipe(); + LineSubBuffer.SetSize(numRows, ColumnCount); // refill it to its previous size + } + else + { + InsertChar('\n'); + } + } + + /// + /// Detect if the interpreter happens to be right at the start of a new command line. + /// + /// true if it's at the start of a new line + public bool IsAtStartOfCommand() + { + return LineBuilder == null || LineBuilder.Length == 0; + } + + public override void Type(char ch) + { + if (!locked) + { + base.Type(ch); + } + } + + public override bool SpecialKey(char key) + { + if (key == (char)UnicodeCommand.BREAK) + { + Shared.Cpu.BreakExecution(true); + LineBuilder.Remove(0, LineBuilder.Length); // why isn't there a StringBuilder.Clear()? + + NewLine(); // process the now emptied line, to make it do all the updates it normally + // does to the screenbuffers on pressing enter. + } + + if (locked) return false; + + switch (key) + { + case (char)UnicodeCommand.UPCURSORONE: + ShowCommandHistoryEntry(-1); + break; + case (char)UnicodeCommand.DOWNCURSORONE: + ShowCommandHistoryEntry(1); + break; + default: + return base.SpecialKey(key); + } + return true; + } + + private void AddCommandHistoryEntry(string commandText) + { + if (commandHistory.Count == 0 || + commandText != commandHistory[commandHistory.Count - 1]) + { + commandHistory.Add(commandText); + } + commandHistoryIndex = commandHistory.Count; + } + + private void ShowCommandHistoryEntry(int deltaIndex) + { + if (commandHistory.Count > 0) + { + int newHistoryIndex = commandHistoryIndex + deltaIndex; + if (newHistoryIndex >= 0 && newHistoryIndex < commandHistory.Count) + { + commandHistoryIndex = newHistoryIndex; + LineBuilder = new StringBuilder(); + LineBuilder.Append(commandHistory[commandHistoryIndex]); + LineCursorIndex = LineBuilder.Length; + MarkRowsDirty(LineSubBuffer.PositionRow, LineSubBuffer.RowCount); + LineSubBuffer.Wipe(); + UpdateLineSubBuffer(); + } + } + } + + public string GetCommandHistoryAbsolute(int absoluteIndex) + { + return commandHistory[absoluteIndex - 1]; + } + + protected virtual void ProcessCommand(string commandText) + { + CompileCommand(commandText); + } + + protected void CompileCommand(string commandText) + { + if (Shared.ScriptHandler == null) return; + + try + { + CompilerOptions options = new CompilerOptions + { + LoadProgramsInSameAddressSpace = false, + FuncManager = Shared.FunctionManager, + IsCalledFromRun = false + }; + + List commandParts = Shared.ScriptHandler.Compile(new InterpreterPath(this), + commandHistoryIndex, commandText, InterpreterName, options); + if (commandParts == null) return; + + var interpreterContext = ((CPU)Shared.Cpu).GetInterpreterContext(); + interpreterContext.AddParts(commandParts); + } + catch (Exception e) + { + if (Shared.Logger != null) + { + Shared.Logger.Log(e); + } + } + } + + public bool IsWaitingForCommand() + { + IProgramContext context = ((CPU)Shared.Cpu).GetInterpreterContext(); + // If running from a boot script, there will be no interpreter instructions, + // only a single OpcodeEOF. So we check to see if the interpreter is locked, + // which is a sign that a sub-program is running. + return !locked && context.Program[context.InstructionPointer] is OpcodeEOF; + } + + public void SetInputLock(bool isLocked) + { + locked = isLocked; + LineSubBuffer.Enabled = !isLocked; + } + + public override void Reset() + { + Shared.ScriptHandler.ClearContext(InterpreterName); + commandHistory.Clear(); + commandHistoryIndex = 0; + } + + public override void PrintAt(string textToPrint, int row, int column) + { + SaveCursorPos(); + base.PrintAt(textToPrint, row, column); + RestoreCursorPos(); + } + + private class InterpreterPath : InternalPath + { + private StandaloneInterpreter interpreter; + + public InterpreterPath(StandaloneInterpreter interpreter) : base() + { + this.interpreter = interpreter; + } + + public override string Line(int line) + { + return interpreter.GetCommandHistoryAbsolute(line); + } + + public override string ToString() + { + return InterpreterName; + } + } + } +} diff --git a/src/kOS.Standalone/StandaloneLogger.cs b/src/kOS.Standalone/StandaloneLogger.cs new file mode 100644 index 0000000000..820e33cd15 --- /dev/null +++ b/src/kOS.Standalone/StandaloneLogger.cs @@ -0,0 +1,200 @@ +using System; +using System.Collections.Generic; +using System.Text; +using kOS.Safe; +using System.Diagnostics; +using kOS.Safe.Persistence; +using kOS.Safe.Compilation; +using kOS.Safe.Execution; +using kOS.Safe.Exceptions; + +namespace kOS.Standalone +{ + class StandaloneLogger : ILogger + { + public StandaloneLogger(StandaloneSharedObjects shared) + { + Shared = shared; + } + + private readonly StandaloneSharedObjects Shared; + + public void Log(string text) + { + Debug.WriteLine(text); + } + + public void Log(Exception e) + { + Debug.WriteLine(e); + + const string LINE_RULE = "__________________________________________\n"; + + string message = e.Message; + + if (kOS.Safe.Utilities.SafeHouse.Config.VerboseExceptions && e is KOSException) + { + // As a first primitive attempt at excercising the verbose exceptions, + // Just use a CONFIG setting for how verbose to be. This will need + // to be replaced with something more sophisticated later, most likely. + + message += "\n" + LINE_RULE + " VERBOSE DESCRIPTION\n"; + + message += ((KOSException)e).VerboseMessage + "\n"; + + message += LINE_RULE; + + // Take on the URL if there is one: + string url = ((KOSException)e).HelpURL; + if (url != String.Empty) + message += "\n\nMore Information at:\n" + url + "\n"; + message += LINE_RULE; + } + + Console.Beep(); + + Shared.Screen.Print(message, true); + string traceText = TraceLog(); + Shared.Screen.Print(traceText, true); + } + + public void LogError(string s) + { + throw new NotImplementedException(); + } + + public void LogException(Exception exception) + { + throw new NotImplementedException(); + } + + public void LogWarning(string s) + { + Debug.WriteLine(s); + } + + public void LogWarningAndScreen(string s) + { + throw new NotImplementedException(); + } + + public void SuperVerbose(string s) + { + Debug.WriteLine(s); + } + + + /// + /// Return a list of strings containing the trace log of the call stack that got to + /// the current point. + /// + /// + private string TraceLog() + { + const string BOGUS_MESSAGE = "(Cannot Show kOS Error Location - error might really be internal. See kOS devs.)"; + + List trace = Shared.Cpu.GetCallTrace(); + string msg = ""; + for (int index = 0; index < trace.Count; ++index) + { + Opcode thisOpcode = Shared.Cpu.GetOpcodeAt(trace[index]); + if (thisOpcode is OpcodeBogus) + { + return BOGUS_MESSAGE; + } + + // The statement "run program" actually causes TWO nested function calls, + // as the logic to check if the program needs compiling is implemented as a + // separate kRISC function that gets called from the main code. Therefore to + // avoid the same RUN statement giving two nested levels on the call trace, + // skip the level of the stack trace that passes through the boilerplate + // load runner code: + if (index > 0) + { + if (thisOpcode.SourcePath == null || thisOpcode.SourcePath.VolumeId.Equals(ProgramBuilder.BuiltInFakeVolumeId)) + { + continue; + } + } + + string textLine = (thisOpcode is OpcodeEOF) ? "<<--EOF" : GetSourceLine(thisOpcode.SourcePath, thisOpcode.SourceLine); + + if (msg.Length == 0) + msg += "At "; + else + msg += "Called from "; + + msg += (thisOpcode is OpcodeEOF) ? "standalone interpreter" + : BuildLocationString(thisOpcode.SourcePath, thisOpcode.SourceLine); + msg += "\n" + textLine + "\n"; + + int useColumn = (thisOpcode is OpcodeEOF) ? 1 : thisOpcode.SourceColumn; + if (useColumn > 0) + { + int numPadSpaces = useColumn - 1; + if (numPadSpaces < 0) + numPadSpaces = 0; + msg += new string(' ', numPadSpaces) + "^" + "\n"; + } + } + return msg; + } + + private string BuildLocationString(GlobalPath path, int line) + { + if (line < 0) + { + // Special exception - if line number is negative then this isn't from any + // line of user's code but from the system itself (like the triggers the compiler builds + // to recalculate LOCK THROTTLE and LOCK STEERING each time there's an Update). + return "(kOS built-in Update)"; + } + + return string.Format("{0}, line {1}", path, line); + } + + private string GetSourceLine(GlobalPath path, int line) + { + string returnVal = "(Can't show source line)"; + + if (line < 0) + { + // Special exception - if line number is negative then this isn't from any + // line of user's code but from the system itself (like the triggers the compiler builds + // to recalculate LOCK THROTTLE and LOCK STEERING each time there's an Update). + return "<>"; + } + + if (path is InternalPath) + { + return (path as InternalPath).Line(line); + } + + Volume vol; + + try + { + vol = Shared.VolumeMgr.GetVolumeFromPath(path); + } + catch (KOSPersistenceException) + { + return returnVal; + } + + VolumeFile file = vol.Open(path) as VolumeFile; + if (file != null) + { + if (file.ReadAll().Category == FileCategory.KSM) + return "<>"; + + string[] splitLines = file.ReadAll().String.Split('\n'); + if (splitLines.Length >= line) + { + returnVal = splitLines[line - 1]; + } + } + + return returnVal; + } + } +} diff --git a/src/kOS.Standalone/StandaloneProcessor.cs b/src/kOS.Standalone/StandaloneProcessor.cs new file mode 100644 index 0000000000..211f8470c7 --- /dev/null +++ b/src/kOS.Standalone/StandaloneProcessor.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Text; +using kOS.Safe.Module; +using kOS.Safe.Persistence; + +namespace kOS.Standalone +{ + class StandaloneProcessor : IProcessor + { + private readonly StandaloneSharedObjects shared; + public StandaloneProcessor(StandaloneSharedObjects shared, string bootfile = null) + { + this.shared = shared; + if (bootfile != null) + { + BootFilePath = VolumePath.FromString(bootfile); + } + } + public VolumePath BootFilePath { get; set; } + + public string Tag => throw new NotImplementedException(); + + public int KOSCoreId => throw new NotImplementedException(); + + public bool CheckCanBoot() + { + return true; + } + + public void SetMode(ProcessorModes newProcessorMode) + { + if (shared.ProcessorMode == ProcessorModes.OFF && newProcessorMode == ProcessorModes.READY) + shared.ProcessorMode = ProcessorModes.STARVED; + else + shared.ProcessorMode = newProcessorMode; + } + } +} diff --git a/src/kOS.Standalone/StandaloneSharedObjects.cs b/src/kOS.Standalone/StandaloneSharedObjects.cs new file mode 100644 index 0000000000..7860b65a53 --- /dev/null +++ b/src/kOS.Standalone/StandaloneSharedObjects.cs @@ -0,0 +1,45 @@ +using System; +using kOS.Safe; +using kOS.Safe.Execution; +using kOS.Safe.Compilation.KS; +using kOS.Safe.Function; +using kOS.Safe.Persistence; +using kOS.Safe.Module; +using kOS.Standalone.Suffixed; + +namespace kOS.Standalone +{ + class StandaloneSharedObjects : SafeSharedObjects + { + public StandaloneSharedObjects() + { + ProcessorMode = ProcessorModes.READY; + + StartTime = DateTime.Now; + + UpdateHandler = new UpdateHandler(); + ScriptHandler = new KSScript(); + Interpreter = new StandaloneInterpreter(this); + Screen = Interpreter; + BindingMgr = new StandaloneBindingManager(this); + Logger = new StandaloneLogger(this); + VolumeMgr = new VolumeManager(); + VolumeMgr.Add(new Archive(kOS.Safe.Utilities.SafeHouse.ArchiveFolder)); + Processor = new StandaloneProcessor(this, "boot/archive"); + FunctionManager = new FunctionManager(this); + GameEventDispatchManager = new StandaloneEventDispatchManager(); + Cpu = new CPU(this); + + StandaloneConnection = new StandaloneConnection(this); + StandaloneCore = new StandaloneCore(this); + StandaloneShip = new StandaloneShip(this); + } + + public ProcessorModes ProcessorMode { get; set; } + public StandaloneConnection StandaloneConnection { get; private set; } + public StandaloneCore StandaloneCore { get; private set; } + public StandaloneShip StandaloneShip { get; private set; } + + public DateTime StartTime { get; set; } + } +} diff --git a/src/kOS.Standalone/Suffixed/StandaloneConnection.cs b/src/kOS.Standalone/Suffixed/StandaloneConnection.cs new file mode 100644 index 0000000000..652e57c4b6 --- /dev/null +++ b/src/kOS.Standalone/Suffixed/StandaloneConnection.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using kOS.Safe.Encapsulation; +using kOS.Safe.Encapsulation.Suffixes; +using kOS.Standalone; + +namespace kOS.Standalone.Suffixed +{ + [kOS.Safe.Utilities.KOSNomenclature("ArchiveConnection")] + class StandaloneConnection : Structure + { + + private readonly StandaloneSharedObjects shared; + public StandaloneConnection(StandaloneSharedObjects shared) + { + this.shared = shared; + AddSuffix("ISCONNECTED", new NoArgsSuffix(() => new BooleanValue(true))); + AddSuffix("DELAY", new NoArgsSuffix(() => ScalarValue.Create(0))); + AddSuffix("DESTINATION", new NoArgsSuffix(() => shared.StandaloneShip)); + } + } +} diff --git a/src/kOS.Standalone/Suffixed/StandaloneCore.cs b/src/kOS.Standalone/Suffixed/StandaloneCore.cs new file mode 100644 index 0000000000..bf15090b89 --- /dev/null +++ b/src/kOS.Standalone/Suffixed/StandaloneCore.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using kOS.Safe.Encapsulation; +using kOS.Safe.Encapsulation.Suffixes; +using kOS.Safe.Persistence; +using kOS.Standalone; + +namespace kOS.Standalone.Suffixed +{ + [kOS.Safe.Utilities.KOSNomenclature("ArchiveCore")] + class StandaloneCore : Structure + { + + private readonly StandaloneSharedObjects shared; + public StandaloneCore(StandaloneSharedObjects shared) + { + this.shared = shared; + AddSuffix("VESSEL", new NoArgsSuffix(() => shared.StandaloneShip)); + AddSuffix("TAG", new NoArgsSuffix(() => new StringValue("Archive"))); + AddSuffix("VOLUME", new NoArgsSuffix(() => shared.VolumeMgr.CurrentVolume)); + AddSuffix("CURRENTVOLUME", new NoArgsSuffix(() => shared.VolumeMgr.CurrentVolume)); + AddSuffix("HOMECONNECTION", new NoArgsSuffix(() => shared.StandaloneConnection)); + } + } +} diff --git a/src/kOS.Standalone/Suffixed/StandaloneShip.cs b/src/kOS.Standalone/Suffixed/StandaloneShip.cs new file mode 100644 index 0000000000..8e778ee9eb --- /dev/null +++ b/src/kOS.Standalone/Suffixed/StandaloneShip.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using kOS.Safe.Encapsulation; +using kOS.Safe.Encapsulation.Suffixes; +using kOS.Standalone; + +namespace kOS.Standalone.Suffixed +{ + [kOS.Safe.Utilities.KOSNomenclature("ArchiveShip")] + class StandaloneShip : Structure + { + + private readonly StandaloneSharedObjects shared; + public StandaloneShip(StandaloneSharedObjects shared) + { + this.shared = shared; + AddSuffix("NAME", new NoArgsSuffix(() => new StringValue("Archive"))); + AddSuffix("CONNECTION", new NoArgsSuffix(() => shared.StandaloneConnection)); + } + } +} diff --git a/src/kOS.Standalone/Suffixed/TimePolyfill.cs b/src/kOS.Standalone/Suffixed/TimePolyfill.cs new file mode 100644 index 0000000000..72a125f292 --- /dev/null +++ b/src/kOS.Standalone/Suffixed/TimePolyfill.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using kOS.Safe.Encapsulation; +using kOS.Safe.Encapsulation.Suffixes; +using kOS.Standalone; + +namespace kOS.Standalone.Suffixed +{ + [kOS.Safe.Utilities.KOSNomenclature("TimePolyfill")] + class TimePolyfill : Structure + { + private readonly DateTime startTime; + public TimePolyfill(DateTime startTime) + { + this.startTime = startTime; + AddSuffix("SECONDS", new NoArgsSuffix(() => (DateTime.Now - startTime).TotalSeconds)); + } + } +} diff --git a/src/kOS.Standalone/kOS.Standalone.csproj b/src/kOS.Standalone/kOS.Standalone.csproj new file mode 100644 index 0000000000..8e34ed1ff2 --- /dev/null +++ b/src/kOS.Standalone/kOS.Standalone.csproj @@ -0,0 +1,16 @@ + + + + Exe + netcoreapp3.1 + true + true + true + win-x64 + + + + + + + diff --git a/src/kOS.sln b/src/kOS.sln index 7231b043e1..7a42531e4d 100644 --- a/src/kOS.sln +++ b/src/kOS.sln @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "kOS.Safe", "kOS.Safe\kOS.Sa EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "kOS.Safe.Test", "kOS.Safe.Test\kOS.Safe.Test.csproj", "{C9A42A44-DDC8-4D6C-8A16-D7F30F494B46}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "kOS.Standalone", "kOS.Standalone\kOS.Standalone.csproj", "{2AD9EFEE-8D18-4176-841D-510D69ADDE40}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +29,10 @@ Global {C9A42A44-DDC8-4D6C-8A16-D7F30F494B46}.Debug|Any CPU.Build.0 = Debug|Any CPU {C9A42A44-DDC8-4D6C-8A16-D7F30F494B46}.Release|Any CPU.ActiveCfg = Release|Any CPU {C9A42A44-DDC8-4D6C-8A16-D7F30F494B46}.Release|Any CPU.Build.0 = Release|Any CPU + {2AD9EFEE-8D18-4176-841D-510D69ADDE40}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2AD9EFEE-8D18-4176-841D-510D69ADDE40}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2AD9EFEE-8D18-4176-841D-510D69ADDE40}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2AD9EFEE-8D18-4176-841D-510D69ADDE40}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/kOS/Binding/BindingManager.cs b/src/kOS/Binding/BindingManager.cs index 20aee02e5f..0fbe60006a 100644 --- a/src/kOS/Binding/BindingManager.cs +++ b/src/kOS/Binding/BindingManager.cs @@ -122,6 +122,15 @@ public bool HasGetter(string name) return variables.ContainsKey(name); } + /// + /// Indicates that the binding should not be cached during execution + /// + /// The binding to modify + public void MarkVolatile(string name) + { + variables[name].Volatile = true; + } + public void PreUpdate() { foreach (var variable in variables) diff --git a/src/kOS/Binding/CPUBinding.cs b/src/kOS/Binding/CPUBinding.cs new file mode 100644 index 0000000000..cf5f17f1c6 --- /dev/null +++ b/src/kOS/Binding/CPUBinding.cs @@ -0,0 +1,15 @@ +using kOS.Safe.Binding; +using kOS.Module; + +namespace kOS.Binding +{ + [Binding("ksp")] + public class CPUBinding : Binding + { + public override void AddTo(SharedObjects shared) + { + shared.BindingMgr.AddGetter("OPCODESLEFT", delegate { return kOSCustomParameters.Instance.InstructionsPerUpdate - shared.Cpu.InstructionsThisUpdate; }); + shared.BindingMgr.MarkVolatile("OPCODESLEFT"); + } + } +} diff --git a/src/kOS/Control/SteeringManager.cs b/src/kOS/Control/SteeringManager.cs index 18e9bb1ff5..ba0f3ec9bd 100644 --- a/src/kOS/Control/SteeringManager.cs +++ b/src/kOS/Control/SteeringManager.cs @@ -667,7 +667,6 @@ public void UpdateTorque() rawTorque.x = (rawTorque.x + PitchTorqueAdjust) * PitchTorqueFactor; rawTorque.z = (rawTorque.z + YawTorqueAdjust) * YawTorqueFactor; rawTorque.y = (rawTorque.y + RollTorqueAdjust) * RollTorqueFactor; - controlTorque = rawTorque + adjustTorque; //controlTorque = Vector3d.Scale(rawTorque, adjustTorque); //controlTorque = rawTorque; @@ -717,6 +716,14 @@ void CorrectedGetPotentialTorque(ITorqueProvider tp, out Vector3 pos, out Vector for (int i = rcs.thrusterTransforms.Count-1; i >= 0; --i) { Transform rcsTransform = rcs.thrusterTransforms[i]; + + // Fixes github issue #2912: As of KSP 1.11.x, RCS parts now use part variants. To keep kOS + // from counting torque as if the superset of all variant nozzles were present, the ones not + // currently active have to be culled out here, since KSP isn't culling them out itself when + // it populates ModuleRCS.thrusterTransforms: + if (!rcsTransform.gameObject.activeInHierarchy) + continue; + Vector3 rcsPosFromCoM = rcsTransform.position - Vessel.CurrentCoM; Vector3 rcsThrustDir = rcs.useZaxis ? -rcsTransform.forward : rcsTransform.up; float powerFactor = rcs.thrusterPower * rcs.thrustPercentage * 0.01f; diff --git a/src/kOS/Screen/Interpreter.cs b/src/kOS/Screen/Interpreter.cs index bdd93e0315..e75faee569 100644 --- a/src/kOS/Screen/Interpreter.cs +++ b/src/kOS/Screen/Interpreter.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Text; using kOS.Execution; diff --git a/src/kOS/Screen/TermWindow.cs b/src/kOS/Screen/TermWindow.cs index 41a6d9a26f..47b8b9d081 100644 --- a/src/kOS/Screen/TermWindow.cs +++ b/src/kOS/Screen/TermWindow.cs @@ -627,7 +627,7 @@ private void ProcessTelnetInput() /// doesn't get the order mixed up. (I.e. use it for paste buffer dumps or telnet input, but not /// live GUI typed stuff.) /// True if the input got consuemed or enqueued. If the input was blocked and not ignored, it returns false. - public bool ProcessOneInputChar(char ch, TelnetSingletonServer whichTelnet, bool allowQueue = true, bool forceQueue = true) + public bool ProcessOneInputChar(char ch, TelnetSingletonServer whichTelnet, bool allowQueue, bool forceQueue) { // Weird exceptions for multi-char data combos that would have been begun on previous calls to this method: switch (inputExpected) @@ -707,6 +707,38 @@ public bool ProcessOneInputChar(char ch, TelnetSingletonServer whichTelnet, bool // else ignore it - unimplemented char. } + /// + /// This is identical to calling ProcessOneInputChar with forceQueue defaulted to true, + /// and it returns void instead of bool. + /// This is being done this way because it has to match exactly to how the + /// signature of the method used to look, to keep it compatible with the DLL for + /// kOSPropMonitor without kOSPropMonitor being recompiled. + /// + /// + /// + /// + /// + public void ProcessOneInputChar(char ch, TelnetSingletonServer whichTelnet, bool allowQueue) + { + ProcessOneInputChar(ch, whichTelnet, allowQueue, true); + } + + /// + /// This is identical to calling ProcessOneInputChar with allowQueu and forceQueue both defaulted to true, + /// and it reutrns void instead of bool. + /// This is being done this way because it has to match exactly to how the + /// signature of the method used to look, to keep it compatible with the DLL for + /// kOSPropMonitor without kOSPropMonitor being recompiled. + /// + /// + /// + /// + /// + public void ProcessOneInputChar(char ch, TelnetSingletonServer whichTelnet) + { + ProcessOneInputChar(ch, whichTelnet, true, true); + } + /// /// Type a normal unicode char (not a magic control char) to the terminal, /// or if the interpreter is busy queue it for later if flags allow. diff --git a/src/kOS/Suffixed/GeoCoordinates.cs b/src/kOS/Suffixed/GeoCoordinates.cs index 453a475fab..c71d1f08f7 100644 --- a/src/kOS/Suffixed/GeoCoordinates.cs +++ b/src/kOS/Suffixed/GeoCoordinates.cs @@ -1,6 +1,7 @@ using UnityEngine; using kOS.Safe.Encapsulation; using kOS.Safe.Encapsulation.Suffixes; +using kOS.Safe.Exceptions; using kOS.Utilities; using kOS.Serialization; using kOS.Safe.Serialization; @@ -176,9 +177,11 @@ public ScalarValue GetTerrainAltitude() // a point a bit below it, to aim down to the terrain: Vector3d worldRayCastStop = Body.GetWorldSurfacePosition( Latitude, Longitude, alt+POINT_AGL ); RaycastHit hit; - if (Physics.Raycast(worldRayCastStart, (worldRayCastStop - worldRayCastStart), out hit, float.MaxValue, 1<Check to find terrain, adding extra masking logic to deal with KSP having put some + /// objects on the terrain layer which weren't really terrain. + private bool RaycastForTerrain(Vector3d worldRayCastStart, Vector3d worldRayCastStop, out RaycastHit hit) + { + Vector3d aimVector = worldRayCastStop - worldRayCastStart; + Vector3d originalStart = worldRayCastStart; + + // The sane way to do this would be to just use a layermask that only hits terrain. + // The problem with trying to do that is KSP's Breaking Ground DLC's rover scanner arms have a + // phantom spherical collider on the terrain layer even though they're really not terrain. + // See my note on Squad bugtracker issue 26938, https://bugs.kerbalspaceprogram.com/issues/26938#note-3) + // To fix that, this contains some extra logic that says if a terrain layer hit turns out to really be + // a vessel part, it should skip past it and keep looking. + int remainingAttempts = 200; + while (Physics.Raycast(worldRayCastStart, aimVector, out hit, float.MaxValue, 1 << TERRAIN_MASK_BIT)) + { + global::Part partHit = hit.collider?.transform?.root?.gameObject?.GetComponent(); + if (partHit == null) + { + // Not a Part, so let's assume its a genuine terrain hit. + + // Return the hit distance from the caller's original start spot, not the temporary one + // we may have moved it to: + hit.distance = (float)(hit.point - originalStart).magnitude; + return true; + } + // Hit was a Part, so it doesn't count. Go again starting from just past that hit: + worldRayCastStart = hit.point + tinySkipDistance * aimVector.normalized; + if (--remainingAttempts == 0) + { + // The majority of the time this loop should only need one iteration. If the + // scene contains any of the few offending parts that use terrain layermask, it will + // need at most one more iteration per offending part in the scene. Anything more than + // just a few iterations you can count on one hand and the algorithm is probably failing. + throw new KOSYouShouldNeverSeeThisException( + "kOS's RaycastForTerrain() is probably stuck in an infinite loop. It's being aborted to prevent it from freezing KSP."); + } + } + return false; + } + /// /// The compass heading from the current position of the CPU vessel to the /// LAT/LANG position on the SOI body's surface. diff --git a/src/kOS/Suffixed/Widget/Button.cs b/src/kOS/Suffixed/Widget/Button.cs index 8fb7c2673d..5fefc857fd 100644 --- a/src/kOS/Suffixed/Widget/Button.cs +++ b/src/kOS/Suffixed/Widget/Button.cs @@ -1,4 +1,4 @@ -using kOS.Safe.Encapsulation; +using kOS.Safe.Encapsulation; using kOS.Safe.Encapsulation.Suffixes; using kOS.Safe.Execution; using UnityEngine; @@ -176,13 +176,22 @@ public override void DoGUI() // Toggles stay pressed until clicked again. // one-shot buttons release as soon as the click is noticed by the script. if (IsToggle) { + myId = GUIUtility.GetControlID(FocusType.Passive); + string myIdString = myId.ToString(); + GUI.SetNextControlName(myIdString); bool newpressed = GUILayout.Toggle(PressedVisible, VisibleContent(), ReadOnlyStyle); if (IsExclusive && !newpressed) return; // stays pressed + if (newpressed != pressed) // if it just toggled on or toggled off + GUI.FocusControl(myIdString); SetPressedVisible(newpressed); } else { + myId = GUIUtility.GetControlID(FocusType.Passive); + string myIdString = myId.ToString(); + GUI.SetNextControlName(myIdString); if (GUILayout.Toggle(PressedVisible, VisibleContent(), ReadOnlyStyle)) { if (!PressedVisible) { PressedVisible = true; + GUI.FocusControl(myIdString); Communicate(() => Pressed = true); } } diff --git a/src/kOS/Suffixed/Widget/Label.cs b/src/kOS/Suffixed/Widget/Label.cs index 4c71704990..f225c3317c 100644 --- a/src/kOS/Suffixed/Widget/Label.cs +++ b/src/kOS/Suffixed/Widget/Label.cs @@ -88,6 +88,8 @@ protected GUIContent VisibleContent() public override void DoGUI() { ScheduleTextUpdate(); + myId = GUIUtility.GetControlID(FocusType.Passive); + GUI.SetNextControlName(myId.ToString()); GUILayout.Label(content_visible, ReadOnlyStyle); } diff --git a/src/kOS/Suffixed/Widget/PopupMenu.cs b/src/kOS/Suffixed/Widget/PopupMenu.cs index 40f6bb76de..5ba4dbdb42 100644 --- a/src/kOS/Suffixed/Widget/PopupMenu.cs +++ b/src/kOS/Suffixed/Widget/PopupMenu.cs @@ -186,17 +186,26 @@ override public void Dispose() public Rect popupRect; private Vector2 rememberScrollSpot = new Vector2(); + private string nameFmt = "{0}_{1}"; + private string indexedName; public void DoPopupGUI() { rememberScrollSpot = GUILayout.BeginScrollView(rememberScrollSpot, popupStyle.ReadOnly); GUILayout.BeginVertical(popupStyle.ReadOnly); for (int i=0; i Index = newindex); SetVisibleText(GetItemString(list[i])); PopDown(); Communicate(() => changed = true); + GUI.FocusControl(indexedName); } } GUILayout.EndVertical(); diff --git a/src/kOS/Suffixed/Widget/Slider.cs b/src/kOS/Suffixed/Widget/Slider.cs index 205dcbf8b7..7550dfa331 100644 --- a/src/kOS/Suffixed/Widget/Slider.cs +++ b/src/kOS/Suffixed/Widget/Slider.cs @@ -1,4 +1,4 @@ -using kOS.Safe.Encapsulation; +using kOS.Safe.Encapsulation; using kOS.Safe.Encapsulation.Suffixes; using kOS.Safe.Execution; using UnityEngine; @@ -51,12 +51,16 @@ private void InitializeSuffixes() public override void DoGUI() { float newvalue; + myId = GUIUtility.GetControlID(FocusType.Passive); + string myIdString = myId.ToString(); + GUI.SetNextControlName(myIdString); if (horizontal) newvalue = GUILayout.HorizontalSlider(valueVisible, min, max, ReadOnlyStyle, thumbStyle.ReadOnly); else newvalue = GUILayout.VerticalSlider(valueVisible, min, max, ReadOnlyStyle, thumbStyle.ReadOnly); if (newvalue != valueVisible) { valueVisible = newvalue; + GUI.FocusControl(myIdString); Communicate(() => Value = newvalue); } } diff --git a/src/kOS/Suffixed/Widget/Spacing.cs b/src/kOS/Suffixed/Widget/Spacing.cs index 73a15a1e26..ca53f9a147 100644 --- a/src/kOS/Suffixed/Widget/Spacing.cs +++ b/src/kOS/Suffixed/Widget/Spacing.cs @@ -1,4 +1,4 @@ -using kOS.Safe.Encapsulation; +using kOS.Safe.Encapsulation; using kOS.Safe.Encapsulation.Suffixes; using UnityEngine; @@ -22,6 +22,7 @@ private void InitializeSuffixes() public override void DoGUI() { + GUI.SetNextControlName(myId.ToString()); if (amount < 0) GUILayout.FlexibleSpace(); else diff --git a/src/kOS/Suffixed/Widget/TextField.cs b/src/kOS/Suffixed/Widget/TextField.cs index 2f85c01780..4b68b9b82e 100644 --- a/src/kOS/Suffixed/Widget/TextField.cs +++ b/src/kOS/Suffixed/Widget/TextField.cs @@ -40,11 +40,6 @@ public bool Confirmed private WidgetStyle emptyHintStyle; - /// - /// Tracks Unity's ID of this gui widget for the sake of seeing if the widget has focus. - /// - private int uiID = -1; - /// /// True if this gui widget had the keyboard focus on the previous OnGUI pass: /// @@ -105,26 +100,41 @@ private void ScheduleOnChange() public override void DoGUI() { bool shouldConfirm = false; - if (GUIUtility.keyboardControl == uiID) - { - if (Event.current.keyCode == KeyCode.Return || Event.current.keyCode == KeyCode.KeypadEnter) - shouldConfirm = true; - hadFocus = true; - } - else + myId = GUIUtility.GetControlID(FocusType.Keyboard); + string myIdString = myId.ToString(); + GUI.SetNextControlName(myIdString); + string newtext = GUILayout.TextField(VisibleText(), ReadOnlyStyle); + if (myId >= 0 && myIdString != null) // skips on the first pass, and on in-between OnGUI passes where sometimes the control Id's are -1 { - if (hadFocus) - shouldConfirm = true; - hadFocus = false; + if (GUI.GetNameOfFocusedControl().Equals(myIdString)) + { + Event thisEvent = Event.current; + if ((thisEvent.keyCode == KeyCode.Return || thisEvent.keyCode == KeyCode.KeypadEnter) + && + // Next condition is because Unity populates the keyCode even when the event has nothing to do + // with keypresses, like on the repaint and layout events that constantly fire every time it + // paints the window. If we do thisEvent.Use() when the event is a repaint or layout instead of + // an actual key event, then Unity aborts drawing the window and it goes away from the screen: + (thisEvent.type == EventType.KeyDown || thisEvent.type == EventType.Used)) + { + shouldConfirm = true; + thisEvent.Use(); + } + hadFocus = true; + } + else + { + if (hadFocus) + shouldConfirm = true; + hadFocus = false; + } } if (shouldConfirm) { Communicate(() => Confirmed = true); - GUIUtility.keyboardControl = -1; } - uiID = GUIUtility.GetControlID(FocusType.Passive) + 1; // Dirty kludge. - string newtext = GUILayout.TextField(VisibleText(), ReadOnlyStyle); + if (newtext != VisibleText()) { SetVisibleText(newtext); Changed = true; diff --git a/src/kOS/Suffixed/Widget/Widget.cs b/src/kOS/Suffixed/Widget/Widget.cs index 2a00e4cd06..42861c9038 100644 --- a/src/kOS/Suffixed/Widget/Widget.cs +++ b/src/kOS/Suffixed/Widget/Widget.cs @@ -30,6 +30,8 @@ abstract public class Widget : Structure /// protected bool guiCaused; + protected int myId; + // The WidgetStyle is cheap as it only creates a new GUIStyle if it is // actually changed, otherwise it just refers to the one in the GUI:SKIN. private WidgetStyle copyOnWriteStyle; diff --git a/src/kOS/kOS.csproj b/src/kOS/kOS.csproj index 59bea5c557..8b9df2eb79 100644 --- a/src/kOS/kOS.csproj +++ b/src/kOS/kOS.csproj @@ -114,6 +114,7 @@ +