diff --git a/doc/source/addons/OrbitalScience.rst b/doc/source/addons/OrbitalScience.rst index 6fd4f56307..dd84f6e758 100644 --- a/doc/source/addons/OrbitalScience.rst +++ b/doc/source/addons/OrbitalScience.rst @@ -13,7 +13,7 @@ Most of the time Orbital Science experiments should work exactly like stock ones they inherit all suffixes from :ref:`ScienceExperimentModule `:: SET P TO SHIP:PARTSTAGGED("")[0]. - SET M TO P:GETMODULE("dmmodulescienceanimate"). + SET M TO P:MODULES:dmmodulescienceanimate. PRINT M:RERUNNABLE. PRINT M:INOPERABLE. @@ -26,7 +26,7 @@ deactivates them:: SET P TO SHIP:PARTSTAGGED("collector")[0]. - SET M TO P:GETMODULE("dmsolarcollector"). + SET M TO P:MODULES:dmsolarcollector. M:TOGGLE. @@ -34,7 +34,7 @@ deactivates them:: lights on and off:: SET P TO SHIP:PARTSTAGGED("bathymetry")[0]. - SET M TO P:GETMODULE("dmbathymetry"). + SET M TO P:MODULES:dmbathymetry. M:LIGHTSON. WAIT 3. diff --git a/doc/source/addons/RemoteTech.rst b/doc/source/addons/RemoteTech.rst index 78d3ea48ad..28c393ee08 100644 --- a/doc/source/addons/RemoteTech.rst +++ b/doc/source/addons/RemoteTech.rst @@ -72,7 +72,7 @@ Antennas It is possible to activate/deactivate RT antennas, as well as set their targets using kOS:: SET P TO SHIP:PARTSNAMED("mediumDishAntenna")[0]. - SET M to p:GETMODULE("ModuleRTAntenna"). + SET M to p:MODULES:ModuleRTAntenna. M:DOEVENT("activate"). M:SETFIELD("target", "Mission Control"). M:SETFIELD("target", mun). diff --git a/doc/source/general/parts_and_partmodules.rst b/doc/source/general/parts_and_partmodules.rst index ff6e8f0014..b515f4f38c 100644 --- a/doc/source/general/parts_and_partmodules.rst +++ b/doc/source/general/parts_and_partmodules.rst @@ -86,7 +86,7 @@ Examples:: // Change the altitude at which all the drogue chutes will deploy: FOR somechute IN somevessel:PARTSNAMED("parachuteDrogue") { - somechute:GETMODULE("ModuleParachute"):SETFIELD("DEPLOYALTITUDE", 1500). + somechute:MODULES:ModuleParachute:SETFIELD("DEPLOYALTITUDE", 1500). }. Pattern matching @@ -217,14 +217,14 @@ B: **Use the :MODULES suffix of Part:** If you have a handle on any part in kOS, LOG P:MODULES TO MODLIST. }. -Do that, and the file MODLIST should now contain a verbose dump of all the module names of all the parts on your ship. You can get any of the modules now by using Part:GETMODULE("module name"). +Do that, and the file MODLIST should now contain a verbose dump of all the module names of all the parts on your ship. You can get any of the modules now by using Part:MODULES:ModuleName. What are the names of the stuff that a PartModule can do? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ These three suffixes tell you everything a part module can do:: - SET MOD TO P:GETMODULE("some name here"). + SET MOD TO P:MODULE:SomeModuleName. LOG ("These are all the things that I can currently USE GETFIELD AND SETFIELD ON IN " + MOD:NAME + ":") TO NAMELIST. LOG MOD:ALLFIELDS TO NAMELIST. LOG ("These are all the things that I can currently USE DOEVENT ON IN " + MOD:NAME + ":") TO NAMELIST. diff --git a/doc/source/structures/misc/config.rst b/doc/source/structures/misc/config.rst index 94a610e5bc..efaeee7a14 100644 --- a/doc/source/structures/misc/config.rst +++ b/doc/source/structures/misc/config.rst @@ -88,6 +88,10 @@ Configuration of kOS - :struct:`Boolean` - False - If true, kOS's controls do nothing. + * - :attr:`DEPRECATEDWARNINGS` + - :struct:`Boolean` + - True + - If true, warnings will be printed when using deprecated globals, suffixes or functions. * - :attr:`TELNET` - :struct:`Boolean` - False @@ -273,6 +277,13 @@ Configuration of kOS While it does suppress steering, throttle, and translation, it cannot suppress action groups or staging. +.. attribute:: Config:DEPRECATEDWARNINGS + + :access: Get/Set + :type: :struct:`Boolean` + + If true, warnings will be printed when using deprecated globals, suffixes or functions. + .. attribute:: Config:TELNET :access: Get/Set diff --git a/doc/source/structures/vessels/part.rst b/doc/source/structures/vessels/part.rst index b2716a3232..96de5a5895 100644 --- a/doc/source/structures/vessels/part.rst +++ b/doc/source/structures/vessels/part.rst @@ -66,21 +66,9 @@ These are the generic properties every PART has. You can obtain a list of values * - :attr:`SHIP` - :struct:`Vessel` - the vessel that contains this part - * - :meth:`GETMODULE(name)` - - :struct:`PartModule` - - Get one of the :struct:`PartModules ` by name - * - :meth:`GETMODULEBYINDEX(index)` - - :struct:`PartModule` - - Get one of the :struct:`PartModules ` by index * - :attr:`MODULES` - - :struct:`List` - - Names (:struct:`String`) of all :struct:`PartModules ` - * - :attr:`ALLMODULES` - - :struct:`List` - - Same as :attr:`MODULES` - * - :meth:`HASMODULE(name)` - - :struct:`Boolean` - - True if the part has the named module in it, false if not. + - :struct:`ModuleLexicon` + - Special :struct:`ModuleLexicon` structure for getting modules. * - :attr:`PARENT` - :struct:`Part` - Adjacent :struct:`Part` on this :struct:`Vessel`. @@ -319,25 +307,47 @@ These are the generic properties every PART has. You can obtain a list of values the vessel that contains this part. -.. method:: Part:GETMODULE(name) +.. attribute:: Part:MODULES + + :access: Get only + :type: :struct:`ModuleLexicon` + + .. structure:: ModuleLexicon + + .. list-table:: Members + :header-rows: 1 + :widths: 1 1 4 - :parameter name: (:struct:`String`) Name of the part module - :returns: :struct:`PartModule` + * - Suffix + - Type + - Description - Get one of the :struct:`PartModules ` attached to this part, given the name of the module. (See :attr:`Part:MODULES` for a list of all the names available). -.. method:: Part:GETMODULEBYINDEX(index) + * - :attr:`PART` + - :struct:`Part` + - The part this ModuleLexicon gets modules from + * - :attr:`KEYS` + - :struct:`List` of strings + - List of module names + * - :meth:`HASKEY(key)` + - :struct:`Boolean` + - Does the part have a module with this name + * - :attr:`LENGTH` + - :struct:`Scalar` + - Number of modules on a part - :parameter index: (:struct:`Scalar`) Index number of the part module - :returns: :struct:`PartModule` + Structure for getting :struct:`PartModules ` from a part with a name or an index. To get a module by its name use the suffix syntax with the module name(the leading "Module" string in the name is optional). Example:: - Get one of the :struct:`PartModules ` attached to this part, - given the index number of the module. You can use :attr:`Part:MODULES` for a - list of names of all modules on the part. The indexes are not guaranteed to - always be in the same order. It is recommended to iterate over the indexes - with a loop and verify the module name:: + print part:modules. + ModuleLexicon, containing keys: + [0] ModuleCommand + [1] ModuleReactionWheel + set command to part:modules:command. + set reactionwheel to part:modules[1]. - local moduleNames is part:modules. + The indexes are not guaranteed to always be in the same order. It is recommended to iterate over the indexes with a loop and verify the module name:: + + local moduleNames is part:modules:keys. for idx in range(0, moduleNames:length) { if moduleNames[idx] = "test module" { local pm is part:getmodulebyindex(idx). @@ -345,28 +355,6 @@ These are the generic properties every PART has. You can obtain a list of values } } - -.. attribute:: Part:MODULES - - :access: Get only - :type: :struct:`List` of strings - - list of the names of :struct:`PartModules ` enabled for this part. - -.. attribute:: Part:ALLMODULES - - Same as :attr:`Part:MODULES` - -.. method:: Part:HASMODULE(name) - - :parameter name: (:struct:`String`) The name of the module to check for - :returns: :struct:`Boolean` - - Checks to see if this part contains the :struct:`PartModule` with the name - given. If it does, this returns true, else it returns false. (If - ``HASMODULE(name)`` returns false, then this means an attempt to use - ``GETMODULE(name)`` would fail with an error.) - .. attribute:: Part:PARENT :access: Get only diff --git a/doc/source/structures/vessels/scienceexperiment.rst b/doc/source/structures/vessels/scienceexperiment.rst index 3112b0362f..6dcdbe3ebf 100644 --- a/doc/source/structures/vessels/scienceexperiment.rst +++ b/doc/source/structures/vessels/scienceexperiment.rst @@ -9,7 +9,7 @@ Some of the science-related tasks are normally not available to kOS scripts. It example possible to deploy a science experiment:: SET P TO SHIP:PARTSNAMED("GooExperiment")[1]. - SET M TO P:GETMODULE("ModuleScienceExperiment"). + SET M TO P:MODULES:ModuleScienceExperiment. M:DOEVENT("observe mystery goo"). However, this results in a dialog being shown to the user. Only from that dialog it is possible @@ -18,7 +18,7 @@ to reset the experiment or transmit the experiment results back to Kerbin. to perform all science-related tasks without any manual intervention:: SET P TO SHIP:PARTSNAMED("GooExperiment")[0]. - SET M TO P:GETMODULE("ModuleScienceExperiment"). + SET M TO P:MODULES:ModuleScienceExperiment. M:DEPLOY. WAIT UNTIL M:HASDATA. M:TRANSMIT. diff --git a/src/kOS.Safe.Test/Execution/Config.cs b/src/kOS.Safe.Test/Execution/Config.cs index 980b9adbe2..0cee1706e1 100644 --- a/src/kOS.Safe.Test/Execution/Config.cs +++ b/src/kOS.Safe.Test/Execution/Config.cs @@ -198,6 +198,19 @@ public bool SuppressAutopilot { } } + + public bool DeprecatedWarnings + { + get + { + return true; + } + + set + { + } + } + public DateTime TimeStamp { get diff --git a/src/kOS.Safe/Encapsulation/IConfig.cs b/src/kOS.Safe/Encapsulation/IConfig.cs index 22bca80619..9583455077 100644 --- a/src/kOS.Safe/Encapsulation/IConfig.cs +++ b/src/kOS.Safe/Encapsulation/IConfig.cs @@ -25,6 +25,7 @@ public interface IConfig: ISuffixed int TerminalDefaultHeight { get; set; } bool AllowClobberBuiltIns { get; set; } bool SuppressAutopilot { get; set; } + bool DeprecatedWarnings { get; set; } /// diff --git a/src/kOS.Safe/Encapsulation/VersionInfo.cs b/src/kOS.Safe/Encapsulation/VersionInfo.cs index aecd7b0a3f..67e9f2913b 100644 --- a/src/kOS.Safe/Encapsulation/VersionInfo.cs +++ b/src/kOS.Safe/Encapsulation/VersionInfo.cs @@ -1,3 +1,4 @@ +using System; using kOS.Safe.Encapsulation.Suffixes; namespace kOS.Safe.Encapsulation @@ -9,7 +10,23 @@ public class VersionInfo : Structure private readonly int minor; private readonly int patch; private readonly int build; + public Version SystemVersion => new Version(major, minor, patch, build); + public VersionInfo(Version version) + { + // NOTICE: there is a clash of nomenclature here. C# calls the + // 3rd number "BUILD" and the 4th number "Revision" while the AVC mod + // (and presumably CKAN) calls the 3rd number "PATCH" and the 4th number "BUILD". + // We'll be using the AVC terminology in kerboscript, thus why this next line + // passes in "ver.Revision" where the VersionInfo's "BUILD" goes, and the + // "ver.Build" where VersionInfo's "PATCH" goes: + major = version.Major; + minor = version.Minor; + patch = version.Build; + build = version.Revision; + VersionInitializeSuffixes(); + } + public VersionInfo(int major, int minor, int patch, int build) { this.major = major; diff --git a/src/kOS.Safe/Utilities/DeprecationHandler.cs b/src/kOS.Safe/Utilities/DeprecationHandler.cs new file mode 100644 index 0000000000..4d312351ca --- /dev/null +++ b/src/kOS.Safe/Utilities/DeprecationHandler.cs @@ -0,0 +1,25 @@ +using kOS.Safe.Encapsulation; +using kOS.Safe.Exceptions; + +namespace kOS.Safe.Utilities +{ + public static class DeprecationHandler + { + private const string DeprecationMessage = + "WARNING: Usage of {0} is being deprecated.\nUse {1} instead.\n"+ + "At kOS version {2} this usage will be removed.\n"+ + "To disable these warnings do \"config:deprecatedwarnings off.\""; + + public static void DeprecatedUsage(SafeSharedObjects shared, string oldUsage, string newUsage, VersionInfo completeDeprecationVersion) + { + if (SafeHouse.Version.SystemVersion >= completeDeprecationVersion.SystemVersion) + { + throw new KOSObsoletionException(completeDeprecationVersion.ToString(), oldUsage, newUsage, null); + } + if (SafeHouse.Config.DeprecatedWarnings) + { + shared.Screen.Print(string.Format(DeprecationMessage, oldUsage, newUsage, completeDeprecationVersion), true); + } + } + } +} diff --git a/src/kOS/Core.cs b/src/kOS/Core.cs index 8ced629dd3..d3d54adf7b 100644 --- a/src/kOS/Core.cs +++ b/src/kOS/Core.cs @@ -20,13 +20,7 @@ public class Core : kOSProcessorFields static Core() { var ver = typeof(Core).Assembly.GetName().Version; - // NOTICE: there is a clash of nomenclature here. C# calls the - // 3rd number "BUILD" and the 4th number "Revision" while the AVC mod - // (and presumably CKAN) calls the 3rd number "PATCH" and the 4th number "BUILD". - // We'll be using the AVC terminology in kerboscript, thus why this next line - // passes in "ver.Revision" where the VersionInfo's "BUILD" goes, and the - // "ver.Build" where VersionInfo's "PATCH" goes: - VersionInfo = new VersionInfo(ver.Major, ver.Minor, ver.Build, ver.Revision); + VersionInfo = new VersionInfo(ver); } public Core(kOSProcessor processor, SharedObjects shared):base(processor, shared) diff --git a/src/kOS/Suffixed/Config.cs b/src/kOS/Suffixed/Config.cs index cae46522f6..1773a1d375 100644 --- a/src/kOS/Suffixed/Config.cs +++ b/src/kOS/Suffixed/Config.cs @@ -35,6 +35,7 @@ public class Config : Structure, IConfig 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; } } public bool SuppressAutopilot { get { return GetPropValue(PropId.SuppressAutopilot); } set { SetPropValue(PropId.SuppressAutopilot, value); } } + public bool DeprecatedWarnings { get { return GetPropValue(PropId.DeprecatedWarnings); } set { SetPropValue(PropId.DeprecatedWarnings, value); } } // NOTE TO FUTURE MAINTAINERS: If it looks like overkill to use a double instead of a float for this next field, you're right. // But KSP seems to have a bug where single-precision floats don't get saved in the config XML file. Doubles seem to work, though. @@ -69,6 +70,7 @@ private void InitializeSuffixes() AddSuffix("DEFAULTWIDTH", new ClampSetSuffix(() => TerminalDefaultWidth, value => TerminalDefaultWidth = value, 15f, 255f, 1f)); AddSuffix("DEFAULTHEIGHT", new ClampSetSuffix(() => TerminalDefaultHeight, value => TerminalDefaultHeight = value, 3f, 160f, 1f)); AddSuffix("SUPPRESSAUTOPILOT", new SetSuffix(() => SuppressAutopilot, value => SuppressAutopilot = value)); + AddSuffix("DEPRECATEDWARNINGS", new SetSuffix(() => DeprecatedWarnings, value => DeprecatedWarnings = value)); } private void BuildValuesDictionary() @@ -92,6 +94,8 @@ private void BuildValuesDictionary() new ConfigKey("TerminalDefaultHeight", "DEFAULTHEIGHT", "Initial Terminal:HEIGHT when a terminal is first opened", 36, 3, 160, typeof(int))); AddConfigKey(PropId.SuppressAutopilot, new ConfigKey("SuppressAutopilot", "SUPPRESSAUTOPILOT", "Suppress all kOS autopiloting for emergency manual control", false, false, true, typeof(bool))); + AddConfigKey(PropId.DeprecatedWarnings, + new ConfigKey("DeprecatedWarnings", "DEPRECATEDWARNINGS", "Enables/disables deprecated warning messages", true, false, true, typeof(bool))); } private void AddConfigKey(PropId id, ConfigKey key) @@ -256,7 +260,8 @@ private enum PropId TerminalBrightness = 17, TerminalDefaultWidth = 18, TerminalDefaultHeight = 19, - SuppressAutopilot = 20 + SuppressAutopilot = 20, + DeprecatedWarnings = 21 } } } diff --git a/src/kOS/Suffixed/Part/PartValue.cs b/src/kOS/Suffixed/Part/PartValue.cs index 7d3b6a78b4..36899daef7 100644 --- a/src/kOS/Suffixed/Part/PartValue.cs +++ b/src/kOS/Suffixed/Part/PartValue.cs @@ -26,6 +26,7 @@ public class PartValue : Structure, IKOSTargetable public Structure ParentValue { get { return (Structure)Parent ?? StringValue.None; } } public Structure DecouplerValue { get { return (Structure)Decoupler ?? StringValue.None; } } public int DecoupledIn { get { return (Decoupler != null) ? Decoupler.Part.inverseStage : -1; } } + private ModuleLexicon modulesLexicon; /// /// Do not call! VesselTarget.ConstructPart uses this, would use `friend VesselTarget` if this was C++! @@ -38,6 +39,8 @@ internal PartValue(SharedObjects shared, global::Part part, PartValue parent, De Decoupler = decoupler; RegisterInitializer(PartInitializeSuffixes); Children = new ListValue(); + + modulesLexicon = new ModuleLexicon(this, shared); } private void PartInitializeSuffixes() @@ -58,10 +61,39 @@ private void PartInitializeSuffixes() AddSuffix("RESOURCES", new Suffix(() => GatherResources(Part))); AddSuffix("TARGETABLE", new Suffix(() => Part.Modules.OfType().Any())); AddSuffix("SHIP", new Suffix(() => VesselTarget.CreateOrGetExisting(Part.vessel, Shared))); - AddSuffix("HASMODULE", new OneArgsSuffix(HasModule)); - AddSuffix("GETMODULE", new OneArgsSuffix(GetModule)); - AddSuffix("GETMODULEBYINDEX", new OneArgsSuffix(GetModuleIndex)); - AddSuffix(new[] { "MODULES", "ALLMODULES" }, new Suffix(GetAllModules, "A List of all the modules' names on this part")); + + VersionInfo oldModuleSystemDeprecationVersion = new VersionInfo(1, 6, 3, 0); + AddSuffix("HASMODULE", new OneArgsSuffix(str => + { + DeprecationHandler.DeprecatedUsage(Shared, ":HASMODULE", ":MODULES:HASKEY", + oldModuleSystemDeprecationVersion); + return HasModule(str); + })); + AddSuffix("GETMODULE", new OneArgsSuffix(str => + { + string newUsage = ":MODULES:" + str; + string trimmedName = ModuleLexicon.TrimModuleName(str); + if (trimmedName != str) + newUsage += " or :MODULES:" + trimmedName; + + DeprecationHandler.DeprecatedUsage(Shared, ":GETMODULE(\"" + str + "\")", newUsage, + oldModuleSystemDeprecationVersion); + return GetModule(str); + })); + AddSuffix("GETMODULEBYINDEX", new OneArgsSuffix(scalar => + { + DeprecationHandler.DeprecatedUsage(Shared, ":GETMODULEBYINDEX(index)", ":MODULES[index]", + oldModuleSystemDeprecationVersion); + return GetModuleIndex(scalar); + })); + AddSuffix("ALLMODULES", new Suffix(() => + { + DeprecationHandler.DeprecatedUsage(Shared, ":ALLMODULES", ":MODULES:KEYS", + oldModuleSystemDeprecationVersion); + return GetAllModules(); + }, "A List of all the modules' names on this part")); + + AddSuffix("MODULES", new Suffix(() => modulesLexicon)); AddSuffix("PARENT", new Suffix(() => ParentValue, "The parent part of this part")); AddSuffix(new[] { "DECOUPLER", "SEPARATOR" }, new Suffix(() => DecouplerValue, "The part that will decouple/separate this part when activated")); AddSuffix(new[] { "DECOUPLEDIN", "SEPARATEDIN" }, new Suffix(() => DecoupledIn)); diff --git a/src/kOS/Suffixed/PartModuleField/ActionsLexicon.cs b/src/kOS/Suffixed/PartModuleField/ActionsLexicon.cs new file mode 100644 index 0000000000..785179f2e9 --- /dev/null +++ b/src/kOS/Suffixed/PartModuleField/ActionsLexicon.cs @@ -0,0 +1,79 @@ +using System; +using kOS.Safe.Encapsulation; +using kOS.Safe.Encapsulation.Suffixes; + +namespace kOS.Suffixed.PartModuleField +{ + [kOS.Safe.Utilities.KOSNomenclature("ActionsLexicon")] + public class ActionsLexicon : Structure + { + private readonly PartModuleFields partModuleFields; + private readonly PartModule partModule; + + public ActionsLexicon(PartModuleFields partModuleFields, PartModule partModule) + { + this.partModuleFields = partModuleFields; + this.partModule = partModule; + + AddSuffix("MODULE", new NoArgsSuffix(() => partModuleFields)); + AddSuffix("KEYS", new Suffix(Keys)); + AddSuffix("HASKEY", new OneArgsSuffix(HasKey)); + AddSuffix("LENGTH", new NoArgsSuffix(() => Keys().Count)); + } + + private ListValue Keys() + { + var list = new ListValue(); + foreach (var action in partModule.Actions) + { + list.Add(new StringValue(action.name)); + } + return list; + } + + private BooleanValue HasKey(StringValue key) + { + foreach (var action in partModule.Actions) + { + if (string.Equals(action.name, key, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + return false; + } + + public override string ToString() + { + string str = "ActionsLexicon of " + partModule.moduleName + ", containing visible keys:"; + foreach (var actions in partModule.Actions) + { + str += "\n" + actions.name + " (" + actions.guiName.ToLower() + ")"; + } + return str; + } + + public override ISuffixResult GetSuffix(string suffixName, bool failOkay = false) + { + foreach (var action in partModule.Actions) + { + if (string.Equals(action.name, suffixName, StringComparison.CurrentCultureIgnoreCase)) + { + return new OneArgsSuffix(value => partModuleFields.CallKSPActionProper(action, value)).Get(); + } + } + + var baseResult = base.GetSuffix(suffixName, true); + if (baseResult != null) return baseResult; + + if (failOkay) return null; + throw new Exception("No key or suffix \""+suffixName+"\" found on "+ToString()); + } + + public override bool SetSuffix(string suffixName, object value, bool failOkay = false) + { + if (failOkay) return false; + throw new Exception("Cannot set suffixes on ActionsLexicon."); + } + } +} diff --git a/src/kOS/Suffixed/PartModuleField/EventsLexicon.cs b/src/kOS/Suffixed/PartModuleField/EventsLexicon.cs new file mode 100644 index 0000000000..10a7f6c07e --- /dev/null +++ b/src/kOS/Suffixed/PartModuleField/EventsLexicon.cs @@ -0,0 +1,86 @@ +using System; +using kOS.Safe.Encapsulation; +using kOS.Safe.Encapsulation.Suffixes; + +namespace kOS.Suffixed.PartModuleField +{ + [kOS.Safe.Utilities.KOSNomenclature("EventsLexicon")] + public class EventsLexicon : Structure + { + private readonly PartModuleFields partModuleFields; + private readonly PartModule partModule; + + public EventsLexicon(PartModuleFields partModuleFields, PartModule partModule) + { + this.partModuleFields = partModuleFields; + this.partModule = partModule; + + AddSuffix("MODULE", new NoArgsSuffix(() => partModuleFields)); + AddSuffix("KEYS", new Suffix(Keys)); + AddSuffix("HASKEY", new OneArgsSuffix(HasKey)); + AddSuffix("LENGTH", new NoArgsSuffix(() => Keys().Count)); + } + + private ListValue Keys() + { + var list = new ListValue(); + foreach (var ev in partModule.Events) + { + if (PartModuleFields.EventIsVisible(ev)) + { + list.Add(new StringValue(ev.name)); + } + } + return list; + } + + private BooleanValue HasKey(StringValue key) + { + foreach (var ev in partModule.Events) + { + if (PartModuleFields.EventIsVisible(ev) && string.Equals(ev.name, key, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + return false; + } + + public override string ToString() + { + string str = "EventsLexicon of " + partModule.moduleName + ", containing visible keys:"; + foreach (var ev in partModule.Events) + { + if (PartModuleFields.EventIsVisible(ev)) + { + str += "\n" + ev.name + " (" + ev.guiName.ToLower() + ")"; + } + } + return str; + } + + public override ISuffixResult GetSuffix(string suffixName, bool failOkay = false) + { + foreach (var ev in partModule.Events) + { + if (PartModuleFields.EventIsVisible(ev) && + string.Equals(ev.name, suffixName, StringComparison.CurrentCultureIgnoreCase)) + { + return new NoArgsVoidSuffix(() => partModuleFields.CallKSPEventProper(ev, suffixName)).Get(); + } + } + + var baseResult = base.GetSuffix(suffixName, true); + if (baseResult != null) return baseResult; + + if (failOkay) return null; + throw new Exception("No key or suffix \""+suffixName+"\" found on "+ToString()); + } + + public override bool SetSuffix(string suffixName, object value, bool failOkay = false) + { + if (failOkay) return false; + throw new Exception("Cannot set suffixes on EventsLexicon."); + } + } +} \ No newline at end of file diff --git a/src/kOS/Suffixed/PartModuleField/FieldsLexicon.cs b/src/kOS/Suffixed/PartModuleField/FieldsLexicon.cs new file mode 100644 index 0000000000..fea00056e5 --- /dev/null +++ b/src/kOS/Suffixed/PartModuleField/FieldsLexicon.cs @@ -0,0 +1,97 @@ +using System; +using kOS.Safe.Encapsulation; +using kOS.Safe.Encapsulation.Suffixes; + +namespace kOS.Suffixed.PartModuleField +{ + [kOS.Safe.Utilities.KOSNomenclature("FieldsLexicon")] + public class FieldsLexicon : Structure + { + private readonly PartModule partModule; + + public FieldsLexicon(PartModuleFields partModuleFields, PartModule partModule) + { + this.partModule = partModule; + + AddSuffix("MODULE", new NoArgsSuffix(() => partModuleFields)); + AddSuffix("KEYS", new Suffix(Keys)); + AddSuffix("HASKEY", new OneArgsSuffix(HasKey)); + AddSuffix("LENGTH", new NoArgsSuffix(() => Keys().Count)); + } + + private ListValue Keys() + { + var list = new ListValue(); + foreach (var field in partModule.Fields) + { + if (PartModuleFields.FieldIsVisible(field)) + { + list.Add(new StringValue(field.name)); + } + } + + return list; + } + + private BooleanValue HasKey(StringValue key) + { + foreach (var field in partModule.Fields) + { + if (PartModuleFields.FieldIsVisible(field) && string.Equals(field.name, key, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + return false; + } + + public override string ToString() + { + string str = "FieldsLexicon of " + partModule.moduleName + ", containing visible keys:"; + foreach (var field in partModule.Fields) + { + if (PartModuleFields.FieldIsVisible(field)) + { + partModule.Fields.TryGetFieldUIControl(field.name, out UI_Control control); + str += "\n" + (control.controlEnabled && !(control is UI_Label) ? "(settable) " : "(get-only) ") + + field.name + " (" + field.guiName.ToLower() + "), is " + + Utilities.Utils.KOSType(field.FieldInfo.FieldType); + } + } + return str; + } + + public override ISuffixResult GetSuffix(string suffixName, bool failOkay = false) + { + // Allows getting "hidden" fields + foreach (var field in partModule.Fields) + { + if (string.Equals(field.name, suffixName, StringComparison.CurrentCultureIgnoreCase)) + { + return new SuffixResult(FromPrimitiveWithAssert(field.GetValue(partModule))); + } + } + + var baseResult = base.GetSuffix(suffixName, true); + if (baseResult != null) return baseResult; + + if (failOkay) return null; + throw new Exception("No key or suffix \""+suffixName+"\" found on "+ToString()); + } + + public override bool SetSuffix(string suffixName, object value, bool failOkay = false) + { + foreach (var field in partModule.Fields) + { + if (string.Equals(field.name, suffixName, StringComparison.CurrentCultureIgnoreCase)) + { + PartModuleFields.SetFieldProper(partModule, field, FromPrimitiveWithAssert(value)); + return true; + } + } + + if (failOkay) return false; + throw new Exception("No key \""+suffixName+"\" found on "+ToString()); + } + } +} diff --git a/src/kOS/Suffixed/PartModuleField/ModuleLexicon.cs b/src/kOS/Suffixed/PartModuleField/ModuleLexicon.cs new file mode 100644 index 0000000000..14fd457d2a --- /dev/null +++ b/src/kOS/Suffixed/PartModuleField/ModuleLexicon.cs @@ -0,0 +1,137 @@ +using System; +using System.Text.RegularExpressions; +using kOS.Safe.Encapsulation; +using kOS.Safe.Encapsulation.Suffixes; +using kOS.Suffixed.Part; + +namespace kOS.Suffixed.PartModuleField +{ + [kOS.Safe.Utilities.KOSNomenclature("ModuleLexicon")] + public class ModuleLexicon : Structure, IIndexable + { + private readonly PartValue part; + private readonly SharedObjects shared; + + public ModuleLexicon(PartValue part, SharedObjects shared) + { + this.part = part; + this.shared = shared; + + AddSuffix("PART", new NoArgsSuffix(() => part)); + AddSuffix("KEYS", new Suffix(GetKeys)); + AddSuffix("HASKEY", new OneArgsSuffix(HasKey)); + AddSuffix("LENGTH", new Suffix(() => part.Part.Modules.Count)); + } + + public static string TrimModuleName(string moduleName) => Regex.Replace(moduleName, "^Module", ""); + + private ListValue GetKeys() + { + var list = new ListValue(); + foreach (PartModule module in part.Part.Modules) + { + list.Add(new StringValue(module.moduleName)); + } + return list; + } + + private BooleanValue HasKey(StringValue key) + { + foreach (PartModule module in part.Part.Modules) + { + if (string.Equals(module.moduleName, key, StringComparison.OrdinalIgnoreCase) || + string.Equals(TrimModuleName(module.moduleName), key, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + return false; + } + + public override string ToString() + { + string str = "ModuleLexicon, containing keys:"; + int i = 0; + foreach (PartModule module in part.Part.Modules) + { + str += "\n[" + i++ + "] " + module.moduleName; + string trimmedName = TrimModuleName(module.moduleName); + if (trimmedName != module.moduleName) + str += " / " + trimmedName; + } + return str; + } + + private PartModuleFields GetModule(string moduleName) + { + // For convenience its possible to access a module without "Module" at the start of its name + // Here we need to make sure module named "Anything" cant be hidden by module "ModuleAnything" + PartModule matchedModule = null; + foreach (PartModule module in part.Part.Modules) + { + if (string.Equals(module.moduleName, moduleName, StringComparison.OrdinalIgnoreCase)) + { + matchedModule = module; + break; + } + if (string.Equals(TrimModuleName(module.moduleName), moduleName, StringComparison.OrdinalIgnoreCase)) + { + matchedModule = module; + } + } + + if (matchedModule == null) + return null; + return PartModuleFieldsFactory.Construct(matchedModule, shared); + } + + public override ISuffixResult GetSuffix(string suffixName, bool failOkay = false) + { + PartModuleFields module = GetModule(suffixName); + + if (module != null) + return new SuffixResult(module); + + var baseResult = base.GetSuffix(suffixName, true); + if (baseResult != null) return baseResult; + + if (failOkay) return null; + throw new Exception("No key or suffix \""+suffixName+"\" found on "+ToString()); + } + + public override bool SetSuffix(string suffixName, object value, bool failOkay = false) + { + if (failOkay) return false; + throw new Exception("Cannot set suffixes on ModuleLexicon."); + } + + public Structure GetIndex(Structure index) + { + if (index is StringValue str) + { + PartModuleFields module = GetModule(str); + if (module != null) + return module; + } + else if (index is ScalarValue scalar) + { + int intIndex = (int)scalar; + if (intIndex >= 0 && intIndex < part.Part.Modules.Count) + { + return PartModuleFieldsFactory.Construct(part.Part.Modules[intIndex], shared); + } + throw new Exception("Index out of range for " + ToString()); + } + throw new Exception("No key \"" + index + "\" found on " + ToString()); + } + + public void SetIndex(Structure index, Structure value) + { + throw new Exception("Cannot set indexes on ModuleLexicon."); + } + + public Structure GetIndex(int index) => GetIndex((Structure)new ScalarIntValue(index)); + + public void SetIndex(int index, Structure value) => SetIndex(null, null); + } +} diff --git a/src/kOS/Suffixed/PartModuleField/PartModuleFields.cs b/src/kOS/Suffixed/PartModuleField/PartModuleFields.cs index f2dc78c691..b9c65178ed 100644 --- a/src/kOS/Suffixed/PartModuleField/PartModuleFields.cs +++ b/src/kOS/Suffixed/PartModuleField/PartModuleFields.cs @@ -23,6 +23,9 @@ public class PartModuleFields : Structure { protected readonly PartModule partModule; protected readonly SharedObjects shared; + private readonly FieldsLexicon fieldsLexicon; + private readonly EventsLexicon eventsLexicon; + private readonly ActionsLexicon actionsLexicon; /// /// Create a kOS-user variable wrapper around a KSP PartModule attached to a part. @@ -33,6 +36,9 @@ public PartModuleFields(PartModule partModule, SharedObjects shared) { this.partModule = partModule; this.shared = shared; + fieldsLexicon = new FieldsLexicon(this, partModule); + eventsLexicon = new EventsLexicon(this, partModule); + actionsLexicon = new ActionsLexicon(this, partModule); // Overriding Structure.InitializeSuffixes() doesn't work because the base constructor calls it // prior to calling this constructor, and so partModule isn't set yet: @@ -58,7 +64,10 @@ public override string ToString() { var returnValue = new StringBuilder(); returnValue.AppendLine(GetModuleName() + ", containing:"); - returnValue.AppendLine(AllThings().ToString()); + foreach (Structure thingString in AllThings()) + { + returnValue.AppendLine(thingString.ToString()); + } return returnValue.ToString(); } @@ -80,7 +89,7 @@ public override string ToString() /// alter the value to an acceptable replacement which is why it passes by ref /// An exception you can choose to throw if you want, or null if the value is legal. /// Is it legal? - private bool IsLegalValue(BaseField field, ref Structure newVal, out KOSException except) + private static bool IsLegalValue(PartModule partModule, BaseField field, ref Structure newVal, out KOSException except) { except = null; bool isLegal = true; @@ -191,7 +200,7 @@ protected virtual ListValue AllFields(string formatter) if ( partModule.Fields.TryGetFieldUIControl(field.name, out control)) { returnValue.Add(new StringValue(string.Format(formatter, - control.controlEnabled ? "settable" : "get-only", + control.controlEnabled && !(control is UI_Label) ? "settable" : "get-only", GetFieldName(field).ToLower(), Utilities.Utils.KOSType(field.FieldInfo.FieldType)))); } @@ -463,14 +472,18 @@ private void InitializeSuffixesAfterConstruction() AddSuffix("ALLHIDDENFIELDNAMES", new Suffix(() => AllFieldNames(field => FieldIsVisible(field, false)))); AddSuffix("HASHIDDENFIELD", new OneArgsSuffix(HasHiddenField)); AddSuffix("GETHIDDENFIELD", new OneArgsSuffix(argument => GetKSPFieldValue(argument, field => FieldIsVisible(field, false)))); + + AddSuffix("FIELDS", new Suffix(() => fieldsLexicon)); + AddSuffix("EVENTS", new Suffix(() => eventsLexicon)); + AddSuffix("ACTIONS", new Suffix(() => actionsLexicon)); } - private bool FieldIsVisible(BaseField field, bool isVisible = true) + public static bool FieldIsVisible(BaseField field, bool isVisible = true) { return (field != null) && (HighLogic.LoadedSceneIsEditor ? field.guiActiveEditor == isVisible : field.guiActive == isVisible); } - private bool EventIsVisible(BaseEvent evt) + public static bool EventIsVisible(BaseEvent evt) { return (evt != null) && ( (HighLogic.LoadedSceneIsEditor ? evt.guiActiveEditor : evt.guiActive) && @@ -512,8 +525,13 @@ protected virtual void SetKSPFieldValue(StringValue suffixName, Structure newVal if (!FieldIsVisible(field)) throw new KOSLookupFailException("FIELD", suffixName, this, true); + SetFieldProper(partModule, field, newValue); + } + + public static void SetFieldProper(PartModule partModule, BaseField field, Structure newValue) + { KOSException except; - if (IsLegalValue(field, ref newValue, out except)) + if (IsLegalValue(partModule, field, ref newValue, out except)) { object convertedValue = Convert.ChangeType(newValue, field.FieldInfo.FieldType); field.SetValue(convertedValue, partModule); @@ -537,12 +555,18 @@ protected virtual void SetKSPFieldValue(StringValue suffixName, Structure newVal /// private void CallKSPEvent(StringValue suffixName) { - ThrowIfNotCPUVessel(); BaseEvent evt = GetEvent(suffixName); if (evt == null) throw new KOSLookupFailException("EVENT", suffixName, this); + + CallKSPEventProper(evt, suffixName); + } + + public void CallKSPEventProper(BaseEvent evt, string eventName) + { + ThrowIfNotCPUVessel(); if (!EventIsVisible(evt)) - throw new KOSLookupFailException("EVENT", suffixName, this, true); + throw new KOSLookupFailException("EVENT", eventName, this, true); if (RemoteTechHook.IsAvailable()) { @@ -565,10 +589,15 @@ private void CallKSPEvent(StringValue suffixName) /// true = activate, false = de-activate private void CallKSPAction(StringValue suffixName, BooleanValue param) { - ThrowIfNotCPUVessel(); BaseAction act = GetAction(suffixName); if (act == null) throw new KOSLookupFailException("ACTION", suffixName, this); + CallKSPActionProper(act, param); + } + + public void CallKSPActionProper(BaseAction act, BooleanValue param) + { + ThrowIfNotCPUVessel(); string careerReason; if (!Career.CanDoActions(out careerReason)) throw new KOSLowTechException("use :DOACTION", careerReason);