

xEvilReeperx
Members-
Posts
894 -
Joined
-
Last visited
Everything posted by xEvilReeperx
-
Stop click-through in KSP 1.1?
xEvilReeperx replied to Angelo Kerman's topic in KSP1 C# Plugin Development Help and Support
In my experience, it isn't worthwhile to set up the new GUI through pure code. It's possible for the simplest of UIs but you'll be much better off working inside the Unity editor in the vast majority of cases. Tutorial-wise, most of the standard UI tutorials apply and there's example code to load from AssetBundles lying around here somewhere -
Stop click-through in KSP 1.1?
xEvilReeperx replied to Angelo Kerman's topic in KSP1 C# Plugin Development Help and Support
HighLogic.Skin.window is a GUIStyle, not a GUILayoutOption. This is the overload I'm using: // UnityEngine.GUILayout public static Rect Window(int id, Rect screenRect, GUI.WindowFunction func, string text, GUIStyle style, params GUILayoutOption[] options) Edit to respond to edit: still isn't an issue That overload uses a temp GUIContent so it isn't allocating -
Stop click-through in KSP 1.1?
xEvilReeperx replied to Angelo Kerman's topic in KSP1 C# Plugin Development Help and Support
The new UI would solve your problem (and it's worthwhile to use: even optimized, the below creates at least 176 bytes in GC allocations per frame), but another option is to take advantage of the EventSystem the new UI uses by adding a raycaster that "hits" your window first and directs all events to it. Here's a hacky example (real world multi-window mod should probably factor out the raycaster code into some kind of manager and enable/disable the mouse controller a bit more intelligently to play nicely with anybody else doing the same) [KSPAddon(KSPAddon.Startup.Flight, false)] class ClickthroughExample : BaseRaycaster { // normal UI stuff private Rect _window = new Rect(0f, 0f, 300f, 300f); // these two save a bit on garbage created in OnGUI private readonly GUILayoutOption[] _emptyOptions = new GUILayoutOption[0]; private GUI.WindowFunction _windowFunction; // enable/disable this to prevent the "No Target" from popping up by double-clicking on the window private Mouse _mouseController; protected override void Awake() { _windowFunction = DrawWindow; _mouseController = HighLogic.fetch.GetComponent<Mouse>(); _window.center = new Vector2(Screen.width * 0.5f, Screen.height * 0.5f); name = "ClickthroughExample"; } private void OnDestroy() { if (_mouseController) _mouseController.enabled = true; } // 88 bytes GCAlloc per call, 2x per frame + 1 for each event private void OnGUI() { _window = KSPUtil.ClampRectToScreen(GUILayout.Window(GetInstanceID(), _window, _windowFunction, "Clickthrough Example", HighLogic.Skin.window, _emptyOptions)); } private void DrawWindow(int winid) { GUILayout.Label("Drag this window around and see if you can click through on anything", _emptyOptions); GUI.DragWindow(); } public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList) { var mouse = Input.mousePosition; var screenPos = new Vector2(mouse.x, Screen.height - mouse.y); if (!_window.Contains(screenPos)) { _mouseController.enabled = true; return; } _mouseController.enabled = false; Mouse.Left.ClearMouseState(); Mouse.Middle.ClearMouseState(); Mouse.Right.ClearMouseState(); resultAppendList.Add(new RaycastResult { depth = 0, distance = 0f, gameObject = gameObject, module = this, sortingLayer = MainCanvasUtil.MainCanvas.sortingLayerID, screenPosition = screenPos }); } public override Camera eventCamera { get { return null; } } public override int sortOrderPriority { get { return MainCanvasUtil.MainCanvas.sortingOrder - 1; } } } -
Have you considered writing a DatabaseLoader instead of using a PartModule? Here's a version of mine I tweaked to handle dependencies like you want that can replace the Mu exporter. Create an AssetBundle with exactly one GameObject prefab in it, rename the extension to "ru" and then treat it exactly like you would a .mu. It supports loading dependencies from KSPs and other RUs. It's still rough proof of concept code though so there are probably some bugs
-
You've got a rogue PartModule being created because of the KSPAddon attribute. That PM isn't associated with any particular Part or Vessel and many of its fields will be unset so your vessel.Find** attempt will throw. OnFixedUpdate is called by the owning Part. The rogue PM isn't owned by any Part and so will never have it called = you don't see any thrown exception.
-
[1.1.3] FlagRotate - Adjust flag orientation
xEvilReeperx replied to xEvilReeperx's topic in KSP1 Mod Releases
Hi, looks like a small change in the API broke it. A simple recompile did the job, updated OP -
Something has gone wrong when you installed a mod called "CryoTanks". There are DLLs inside GameData\CryoTanks\Plugins that should absolutely not be there. Delete every dll inside that folder except SimpleBoiloff.dll (or better yet, download the new version released by @Nertea as he's already fixed the problem). This is probably your issue
-
IPartCostModifier was changed. Some details can be found in the link. The other problem is that all EzGUI stuff was migrated over to the Unity UI. EzGUI is present in the assemblies but I don't believe it's used at all anywhere anymore. toggleCoLbtn is now a UnityEngine.UI.Button so you'll want to change AddValueChangedDelegate into .onClick.AddListener
-
Hmmm I'm not seeing this behaviour in a simple test case. This works as expected: class FloatSliderActingLikeMaxValueStatic : PartModule { [KSPField(guiName = "Test Field", isPersistant = true, guiActive = true, guiActiveEditor = true), UI_FloatRange(stepIncrement = 0.5f, maxValue = 100f, minValue = 0f)] public float someField = 100f; public override void OnStart(StartState state) { base.OnStart(state); if (part.partInfo.name == "mk1pod") { Log.Warning("Setting values for pod"); ((UI_FloatRange) Fields["someField"].uiControlEditor).minValue = 20f; ((UI_FloatRange)Fields["someField"].uiControlEditor).maxValue = 220f; } else { Log.Warning("Setting values for can"); ((UI_FloatRange)Fields["someField"].uiControlEditor).minValue = 10f; ((UI_FloatRange)Fields["someField"].uiControlEditor).maxValue = 110f; } } } [KSPAddon(KSPAddon.Startup.SpaceCentre, true)] class FloatSliderTestInstaller : MonoBehaviour { private void Start() { DontDestroyOnLoad(this); var pod = PartLoader.getPartInfoByName("mk1pod").partPrefab; var can = PartLoader.getPartInfoByName("landerCabinSmall").partPrefab; pod.gameObject.AddComponent<FloatSliderActingLikeMaxValueStatic>(); can.gameObject.AddComponent<FloatSliderActingLikeMaxValueStatic>(); } } Is there any more code in there? The only gotcha I can think of is that having the UI_Control affect sym counterparts could possibly overwrite minValue and maxValue (didn't check) but you mentioned two distinct parts so I wouldn't think that was your problem
-
I'm working on it as time allows and the main constraint is that I just don't have a ton of it. I haven't really posted because I keep telling myself "well, it's almost done and I can release in a few days, I'll wait until then" and then RL strikes or I run into a snag that requires a bunch of rethinking and before I know it, yet another week has passed
- 962 replies
-
- 30
-
-
You might want to try just creating a line where you want it before you throw in all the orbit stuff. There's at least two errors in there: you're creating a mesh in scaled space but trying to use points in local space, and 2) this makes no sense: Vector3 PositionAt1stDegree = orbit.getTruePositionAtUT(time + (TimeSnapshots * (FutureRenderOrbits.IndexOf(orbit) + (1.0 / 6.0)))); Vector3 PositionAt2ndDegree = orbit.getTruePositionAtUT(time + (TimeSnapshots * (FutureRenderOrbits.IndexOf(orbit) + (2.0 / 6.0)))); // etc Points3D[0] = camera.ScreenToWorldPoint(PositionAt1stDegree); Points3D[1] = camera.ScreenToWorldPoint(PositionAt2ndDegree); // etc 1) you're taking a point in world space and passing it through a transformation like it was in screen space (oversight?) 2) the camera you're using for the transformation is set up for scaled world space. Convert your vectors from local to scaled space before trying to use the planetarium camera with them
-
An exception with a trace that looks like that is usually a good sign that the native side of the object is missing, usually because the component/GO was destroyed or because it was created incorrectly. Both MeshFilter and MeshRenderer are Components which must be attached to a GameObject and should never be created using new, so these lines are definitely wrong (maybe just an oversight?): MeshRenderer OrbitBezierRenderer= new MeshRenderer(); MeshFilter OrbitBezierFilter = new MeshFilter(); This is why you're seeing the exception. The very first run through the foreach, gameObject* doesn't have a MeshRenderer or MeshFilter so the values from the above are tossed out and the correct method to create them is used. The second iteration then attempts to use the wrongly constructed object because the script's GO has a MeshRenderer and MeshFilter now. * note your logic error there: surely you intended to use the GameObject [BezierOrbit] you've created for the orbit, in which case the MeshRenderer/MeshFilter checks are unnecessary anyway
-
Request: Font Outline mod to make text more readable
xEvilReeperx replied to Fictitious1267's topic in KSP1 Mods Discussions
It's actually rather straightforward. The prefab that gets used for all the mouseover-type stuff in the MapView is exposed so you can just slip in an Outline component onto the text portion of that and bam, instant text outlines. This is all the code you'd need to get a basic effect like you want: using System.Linq; using UnityEngine; using UnityEngine.UI; namespace MapViewOutline { [KSPAddon(KSPAddon.Startup.Flight, true)] class AddOutlineToMapViewTextInFlight : AddOutlineToMapViewText { } [KSPAddon(KSPAddon.Startup.TrackingStation, true)] class AddOutlinetoMapViewInTrackingStation : AddOutlineToMapViewText { } abstract class AddOutlineToMapViewText : MonoBehaviour { private const float HorizontalEffect = 0.8f; private const float VerticalEffect = 0.8f; private readonly Color _color = Color.black; private static bool _ranFlag = false; private void Start() { if (!_ranFlag) AddOutline(); _ranFlag = true; Destroy(gameObject); } private void AddOutline() { // Search for "Caption" text. There's only one. Use version of GetComponents that searches inactive GO like this prefab var text = MapView.UINodePrefab.GetComponentsInChildren<Text>(true).FirstOrDefault(); if (text == null) { Debug.LogError("Didn't find expected Text component in UINodePrefab: can't add outline"); return; } var outline = text.gameObject.AddComponent<Outline>(); outline.effectColor = _color; outline.effectDistance = new Vector2(HorizontalEffect, VerticalEffect); outline.useGraphicAlpha = true; } } } These screenshots have outlines in red rather than black because it's much easier to tell it's working in stock conditions -
The easiest option is going to be just tossing 10-15 KSPFields in there and hiding the ones you aren't using. If you're dead-set on having your cake though, you can use your list as the backing field and then write a UI_Control that displays it to your liking.
- 16 replies
-
- kspfield
- kspapiextensions
-
(and 1 more)
Tagged with:
-
KSPField questions
xEvilReeperx replied to linuxgurugamer's topic in KSP1 C# Plugin Development Help and Support
[Persistent] supports Lists and arrays. Simple example: class PersistentPartModuleExample : PartModule { [Persistent] private string[] _testArray = { "Apple", "Pear" }; [Persistent] private List<string> _testList = new List<string> { "Cucumber", "Carrot" }; public override void OnSave(ConfigNode node) { node.AddNode(ConfigNode.CreateConfigFromObject(this)); } public override void OnLoad(ConfigNode node) { ConfigNode.LoadObjectFromConfig(this, node.GetNode(GetType().FullName)); } } It also executes methods before and after serializing the object if the object implements IPersistenceSave and IPersistenceLoad. You can easily save a dictionary (or resolve a reference, set a field, etc) this way: class PersistentPartModuleExample : PartModule, IPersistenceSave, IPersistenceLoad { // can't be serialized private Dictionary<string, string> _myDictionary = new Dictionary<string, string>(); [Persistent] private List<string> _dictionaryKeys = new List<string>(); [Persistent] private List<string> _dictionaryValues = new List<string>(); public override void OnSave(ConfigNode node) { node.AddNode(ConfigNode.CreateConfigFromObject(this)); } public override void OnLoad(ConfigNode node) { ConfigNode.LoadObjectFromConfig(this, node.GetNode(GetType().FullName)); } // just before serializing public void PersistenceSave() { _dictionaryKeys = _myDictionary.Keys.ToList(); _dictionaryValues = _myDictionary.Values.ToList(); } // just after deserializing public void PersistenceLoad() { _myDictionary = Enumerable.Range(0, _dictionaryKeys.Count) .ToDictionary(idx => _dictionaryKeys[idx], idx => _dictionaryValues[idx]); } } You can also serialize somewhat complex types with it. Maybe you have a class defining some information you want to persist. This particular one would be somewhat unpleasant to serialize manually (note its recursive nature): class PersistentPartModuleExample : PartModule { class ImportantDataToPersist { [Persistent] public string ImportantThing = string.Empty; [Persistent] public double ImportantNumber; // note how it's a double? handled! [Persistent] public List<ImportantDataToPersist> Children = new List<ImportantDataToPersist>(); } [Persistent] private ImportantDataToPersist _importantStuff = new ImportantDataToPersist(); public override void OnLoad(ConfigNode node) { ConfigNode.LoadObjectFromConfig(this, node.GetNode(GetType().FullName)); } public override void OnSave(ConfigNode node) { node.AddNode(ConfigNode.CreateConfigFromObject(this)); } } -
It is, that's why I defined my own (also in code above). You might want to be more discerning about the contracts you save, though. The player can't accept contracts in flight barring some kind of mod and saving the contracts can be garbagey and slow. I've seen a mod at some point that constantly rejected certain contracts which would be a performance and garbage collection nightmare for you
-
It's a lot easier to catch them when they're offered rather than after the contract has been accepted and the related parts have spawned. You can get around parsing HighLogic.CurrentGame by saving (and editing) the contract yourself to change those private variables [KSPAddon(KSPAddon.Startup.SpaceCentre, false)] class RescueContractsUseLanderCans : MonoBehaviour { private void Start() { GameEvents.Contract.onOffered.Add(OnContractOffered); } private void OnDestroy() { GameEvents.Contract.onOffered.Remove(OnContractOffered); } private enum RecoveryContractType { Kerbal = 1, // lone Kerbal, maybe in a part Part = 2, // Recover a part Compound = 3 // lone kerbal, plus also recover a part(s) } private class RecoveryContractInfo { [Persistent] public RecoveryContractType recoveryType; [Persistent] public string partName; [Persistent] public uint partID; private static bool EnumWithinRange<TEnum>(TEnum value) { return Enum.GetValues(typeof (TEnum)).Cast<TEnum>().Contains(value); } public bool IsValid() { return partID == 0 /* otherwise the contract was generated already */ && !string.IsNullOrEmpty(partName) && EnumWithinRange(recoveryType); } } private static void OnContractOffered(Contract data) { var recoveryContract = data as RecoverAsset; if (recoveryContract == null) return; var contractConfig = new ConfigNode("Contract"); data.Save(contractConfig); var info = new RecoveryContractInfo(); if (!ConfigNode.LoadObjectFromConfig(info, contractConfig) || !info.IsValid()) { Log.Error("Something went wrong while inspecting a recovery contract!"); return; } if (info.recoveryType == RecoveryContractType.Part) return; // standard part recovery, if it doesn't involve a kerbal we don't care // All kerbal and compound recovery missions shall henceforth use the venerable landerCabinSmall contractConfig.SetValue("partName", "landerCabinSmall"); Contract.Load(data, contractConfig); } }
-
I guess I didn't do a good job explaining. The problem is that parts defined using MODEL{} nodes have the GameDatabase model cloned. This is an issue for Transform.Find because the GameDatabase model top-level transform gets renamed to its url inside GameData, so "GameObject > base > gate_1_root > gate_1 > gate_1_collider_1" is renamed to "YourPluginFolder/muname > base > gate_1_root > gate_1 > gate_1_collider_1". The overwritten name now contains slashes which Transform.Find will incorrectly interpret. The two problems I have with your solution is that it will fail for parts that aren't defined using MODEL{} and the only way to specify the uppermost node of the model would for your code to check for a blank path (because that's what part.transform.GetChild(0).GetChild(n) will return) Edit: It might make it clearer just to show you the hierarchy dumps. Here's what my test part looks like in Unity: Here's what it looks like loaded from the regular mesh = model.mu style inside a part ConfigNode: Here's the same part, loaded via MODEL{} I do like the idea of ignoring the URL transform like you have though, it lets you clean up the earlier method a bit to this: private static Transform FindModelTransformEx(Part part, string transformPath) { if (string.IsNullOrEmpty(transformPath)) throw new ArgumentException("must specify a url", "transformPath"); var pathParts = transformPath.Trim('/').Split('/'); var modelTransform = part.transform.Find("model"); if (pathParts.Length == 1) { if (modelTransform.childCount == 1) return modelTransform.GetChild(0); // no way to tell for certain this transform name matches without parsing the mu again throw new ArgumentException( "The part has multiple models and there isn't enough information to figure out which one " + transformPath + " specifies"); } var targetPath = string.Join("/", pathParts.Skip(1).ToArray()); if (!part.partInfo.partConfig.HasNode("MODEL")) return pathParts.Length == 1 ? modelTransform : modelTransform.Find(targetPath); foreach (Transform model in modelTransform) { var found = model.transform.Find(targetPath); if (found) return found; } return null; }
-
I found some time this morning to fix it. It works a little differently now so I've updated the instructions on the first page. Let me know if there are any problems
-
That's part of my own library; you'd need to replace them with your own log statements or Debug.Log. When GameDatabase loads the part models, their top level transform names get replaced with the URL of the model. That's why Find() is choking for you... Unity is treating the forward slashes like transform separators. You'll have to avoid using Unity's transform.Find for parts with MODEL nodes defined. Also apparently the model transform is no longer its own GameObject in parts without MODEL nodes; the top-level model transform name just gets overwritten with "model". This is a bit of code I tinkered with. Your input here would be as though you were referring to the hierarchy inside Unity, so the url argument for your case would be "GameObject/base/gate_shield_root". It was not tested for parts with multiple MODELs at all. There's a wrinkle there: because the game overwrites the original top level node name, you won't be able to distinguish between multiple MODELs if only searching for the top node. That might or might not be a problem. If it was serious enough, you could parse the original Mu to read it back again if you had to private static Transform FindModelTransformEx(Part part, string url) { if (!part.partInfo.partConfig.HasNode("MODEL")) { return part.transform.Find(string.Join("/", new[] {"model"}.Union(url.Split('/').Skip(1)).ToArray())); } foreach (var modelConfig in part.partInfo.partConfig.GetNodes("MODEL")) { var transformNames = new Queue<string>(); var modelUrl = modelConfig.GetValue("model"); var pathPortions = url.Split('/'); if (string.IsNullOrEmpty(modelUrl)) continue; transformNames.Enqueue("model"); pathPortions[0] = modelUrl + "(Clone)"; foreach (var portion in pathPortions) transformNames.Enqueue(portion); var result = FindTransform(part.transform, transformNames); if (result != null) return result; } return null; } private static Transform FindTransform(Transform search, Queue<string> transformNames) { if (!transformNames.Any()) return null; var target = transformNames.Dequeue(); foreach (Transform child in search) if (child.name == target) { if (transformNames.Any()) return FindTransform(child, transformNames); return child; } return null; }
-
Are you sure that's what it printed? Does your part config use MODEL nodes? What does the hierarchy actually look like in game? That would tell us a lot. Here's a snippet you can edit if you don't already have something similar: public static class GameObjectExtensions { public delegate void VisitorDelegate(GameObject go, int depth); public static void TraverseHierarchy(this GameObject go, VisitorDelegate visitor) { if (go == null) throw new ArgumentNullException("go"); if (visitor == null) throw new ArgumentNullException("visitor"); TraverseHierarchy(go, visitor, 0); } private static void TraverseHierarchy(GameObject go, VisitorDelegate visitor, int depth) { visitor(go, depth); foreach (Transform t in go.transform) TraverseHierarchy(t.gameObject, visitor, depth + 1); } public static void PrintComponents(this GameObject go, ILog baseLog) { if (go == null) throw new ArgumentNullException("go"); if (baseLog == null) throw new ArgumentNullException("baseLog"); go.TraverseHierarchy((gameObject, depth) => { baseLog.Debug("{0}{1} has components:", depth > 0 ? new string('-', depth) + ">" : "", gameObject.name); var components = gameObject.GetComponents<Component>(); foreach (var c in components) { baseLog.Debug("{0}: {1}", new string('.', depth + 3) + "c", c == null ? "[missing script]" : c.GetType().FullName); } }); } } Edit: Also Find() would fail in your case because you've appended the name of the parent itself. If HX1AerodynamicGateShell/model/0000_MyParts/HX_1_Aerodynamic_Gate_Shell/model(Clone)/base/gate_shield_root is what your code printed, your PartModule (which is attached to that top transform) should transform.Find("model/0000_MyParts/HX_1_Aerodynamic_Gate_Shell/model(Clone)/base/gate_shield_root")
-
[1.1.2] GrumpyCollector 1.0.1 - Stutter ? What Stutter
xEvilReeperx replied to sarbian's topic in KSP1 Mod Releases
Yes, at least the big ticket or easy pickings items. I do get what you're saying though, I just wanted to know if it was something you were looking at. Are you reporting these on the bug tracker? I have a whole mountain of them I could contribute. Some are incredibly easy to fix, such as the one related to GC being proportional to number of on rails parts. If you crystal ball the reason for this one, it's so dumb it makes me wonder if they're profiling at all: // TetraTetra save