Jump to content


  • Posts

  • Joined

  • Last visited


1,089 Excellent

1 Follower

Profile Information

  • About me
    Junior Rocket Scientist

Recent Profile Visitors

The recent visitors block is disabled and is not being shown to other users.

  1. You can use the same general idea. In this case, it's easier to straight hijack the live button once it exists. Most of the effort of this is figuring out how to get a reference to the thing you want (conveniently, RDController.Instance.actionButton) and how to manipulate it in a way that doesn't break stock code. Here is some prototype code to get you started: /* // RDController.Instance.nodePanel Panel_Right has components: ...c: UnityEngine.RectTransform ...c: KSP.UI.UIStatePanel ->Panel node has components: <-- this is the RnD panel (there are two panels in this GUI: RnD and archives) ....c: UnityEngine.RectTransform ....c: UnityEngine.CanvasRenderer ....c: UnityEngine.UI.Image -->Button has components: .....c: UnityEngine.RectTransform .....c: UnityEngine.CanvasRenderer .....c: UnityEngine.UI.Image .....c: UnityEngine.UI.Button <-- this fires clicks .....c: KSP.UI.UIStateButton <-- this listens to Button and is involved in the visual state of the button --->Text has components: ......c: UnityEngine.RectTransform ......c: UnityEngine.CanvasRenderer ......c: TMPro.TextMeshProUGUI <-- you can use this to manipulate the button text ---->TMP UI SubObject [TextMeshPro/Sprite] has components: <-- I assume this has something to do with the science graphic in the button text .......c: UnityEngine.RectTransform .......c: UnityEngine.CanvasRenderer .......c: TMPro.TMP_SubMeshUI */ [KSPAddon(KSPAddon.Startup.SpaceCentre, true /* run once */)] class ConditionalResearchButtonSetup : MonoBehaviour { private void Start() { RDController.OnRDTreeSpawn.Add(i => AddButton(i)); // EventData doesn't seem to like non-static methods Destroy(gameObject); } // inserts conditional button into RnD instance private static void AddButton(RDController instance) { instance.actionButton.gameObject.AddComponent<ConditionalResearchButton>(); } } // Conditional research button. When player clicks a technology, fires a message // allowing recipients to deny player access to the button // // Also adds confirmation dialog as an example of how you can intercept an event entirely class ConditionalResearchButton : MonoBehaviour { public class Request { public RDNode node; public bool allowed; public void Deny() { allowed = false; } } public static EventData<Request> OnResearchButtonPresented = new EventData<ConditionalResearchButton.Request>("OnResearchButtonPresented"); private Image _buttonImage; private Button _button; private Button.ButtonClickedEvent _originalEvent; private void Awake() { // Start is too slow to grab these references if we want to use OnEnable _buttonImage = GetComponent<Image>(); _button = GetComponent<Button>(); // hijack button so we can decide whether it's been "clicked" _originalEvent = _button.onClick; _button.onClick = new Button.ButtonClickedEvent(); _button.onClick.AddListener(OnButtonClicked); // the button we have is the "raw" button, but a lot of state-driven buttons // have another class controlling visual states. RDController sets the // state each time the panel changes for some reason, which is a good enough // way for us to stay informed about selection changes GetComponent<UIStateButton>().onValueChanged.AddListener(_ => UpdateButtonStatus()); } private void OnEnable() { UpdateButtonStatus(); } // if the button is used for research, decide if the player can click it private void UpdateButtonStatus() { // don't mess with the button if it's not a research button (seems to serve dual purposes?) // otherwise, if it's a research button, decide if it should be enabled ToggleButton(!IsNodeResearchable() || AllowResearch()); } private void ToggleButton(bool enable) { _button.enabled = enable; // color difference to clarify button status _buttonImage.color = enable ? Color.white : Color.red; } private void OnButtonClicked() { // here you could decide not to call original listeners // as an example, I'll add a confirmation window if (IsNodeResearchable()) { PopupDialog.SpawnPopupDialog( new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new MultiOptionDialog( "ConfirmResearchDialog", $"Are you sure you want to purchase {RDController.Instance.node_selected.tech.title}?", "Confirm?", UISkinManager.GetSkin("KSP window 7"), new DialogGUIButton("Yes", UserConfirmedPurchase, true), new DialogGUIButton("No", () => { }, true)), false, UISkinManager.GetSkin("KSP window 7")); } else { _originalEvent.Invoke(); // not a research event } } // user confirmed they want to research whatever it is that's active private void UserConfirmedPurchase() { _originalEvent.Invoke(); // just as though user clicked the button themselves } // user can select already-researched nodes. We don't care about those private bool IsNodeResearchable() { var node = RDController.Instance.node_selected; return node != null && node.state == RDNode.State.RESEARCHABLE; } // fires event to decide if the button is enabled private bool AllowResearch() { var request = new Request { node = RDController.Instance.node_selected, allowed = true }; OnResearchButtonPresented.Fire(request); return request.allowed; } } // An addon that wants to control whether or not the player can research something [KSPAddon(KSPAddon.Startup.SpaceCentre, false)] class LetsStayLowTech : MonoBehaviour { private void Start() { ConditionalResearchButton.OnResearchButtonPresented.Add(ShouldWeAllowIt); } private void OnDestroy() { ConditionalResearchButton.OnResearchButtonPresented.Remove(ShouldWeAllowIt); } private void ShouldWeAllowIt(ConditionalResearchButton.Request request) { // nobody gets to use techs that cost more than 50 science // note: might need to apply game modifiers here if (request.node.tech.scienceCost > 50f) request.Deny(); } }
  2. Don't mess with the original models in GameDatabase. Instantiate a new GameObject using them instead or you'll blow things up later. I would be surprised if your first log was working; GetComponentInChildren only returns a component on active GameObjects (unless you use one of its overloads) and none of the model stuff should be active
  3. I've put together a bare-bones example that does what you want. Code: using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using UnityEngine; using KSPAssets; namespace CargoLoader { [DatabaseLoaderAttrib(new[] { "cargo" })] public class CargoLoader : DatabaseLoader<GameObject> { private readonly List<AssetBundle> _loadedBundles = new List<AssetBundle>(); private void Failed(string reason) { successful = false; Debug.LogError(reason); } public override IEnumerator Load(UrlDir.UrlFile urlFile, FileInfo file) { // load the AssetBundle var bundleRequest = AssetBundle.LoadFromFileAsync(file.FullName); yield return bundleRequest; var bundle = bundleRequest.assetBundle; if (bundle == null) { Failed("Unable to load: " + urlFile.url); yield break; } _loadedBundles.Add(bundle); // use manifest to determine URLs of models // // -> first we need to find it. It's probably named the same as the // bundle, but let's not rely on that in case it gets renamed var manifest = bundle.GetAllAssetNames().FirstOrDefault( assetName => assetName.EndsWith("_bundle.xml")); if (string.IsNullOrEmpty(manifest)) { Failed("Could not find manifest inside " + urlFile.url); foreach (var name in bundle.GetAllAssetNames()) { Debug.Log(" name: " + name); } yield break; } var manifestRequest = bundle.LoadAssetAsync<TextAsset>(manifest); yield return manifestRequest; if (manifestRequest.asset == null) { Failed("Failed to load manifest: " + manifest); yield break; } var xmlManifest = (TextAsset)manifestRequest.asset; var bundleDefinition = BundleDefinition.CreateFromText(xmlManifest.text); bundleDefinition.path = urlFile.fullPath; foreach (var def in bundleDefinition.GetAssetsWithType(typeof(GameObject))) { var assetRequest = bundle.LoadAssetAsync<GameObject>(def.name); yield return assetRequest; var asset = assetRequest.asset as GameObject; if (asset == null) { Debug.LogWarning(string.Format("Failed to load asset '{0}' from {1}", def.name, urlFile.url)); // don't quit here: maybe there are multiple assets in this bundle } // two options here: use the Unity path, or just take the name and generate // a URL using the path of the bundle. Latter option makes more sense to me // note: this means the name of the bundle is used within the part's url // example: GameData/myfolder/mybundle.cargo has two parts, part1 and part2, inside it // you would reference these parts in a config as myfolder/mybundle/part1 var name = Path.GetFileNameWithoutExtension(def.name); var url = urlFile.parent.url + "/" + urlFile.name + "/" + name; asset.name = url; asset.transform.parent = GameDatabase.Instance.transform; GameDatabase.Instance.databaseModel.Add(asset); // this next line might be a problem if you have multiple models in one bundle // unsure when and where it's used GameDatabase.Instance.databaseModelFiles.Add(urlFile); Debug.Log("Loaded " + url + " from " + urlFile.url); } } public override void CleanUp() { base.CleanUp(); foreach (var bundle in _loadedBundles) bundle.Unload(false); } } } This will load all prefab GameObjects from a KSP bundle created with PartTools (renamed with .cargo extension) and make them available for use from MODEL nodes inside part configs. It does nothing extra or fancy (no dependency resolution) but just cramming everything into one bundle is convenient and ensures resources are shared. The bundle I've provided contains two prefabs named Cube and Sphere. Cube uses a custom shader I slapped together. Sphere uses one of the shaders from PartTools. Both have a shader property driven by an animation. You refer to these in the same way that you would refer to a mu file, with the exception that the bundle is part of the path. For example, if you have the bundle at GameData/mystuff/mybundle.cargo, you would refer to the assets inside using a config file like this: PART { // ... MODEL { model = mystuff/mybundle/Sphere // ... } // ... }
  4. The game scans for all DatabaseLoaderAttrib-decorated types and uses them to load files from GameData directory. You can use this to have the game do stuff with your own files. I recommend you choose your own extension (it can be whatever you like that isn't already taken). It looks like ShadowMage needed to manipulate the materials, and uses that list to keep track of materials that have already been adjusted. My prototype didn't need to do this because I stored everything (textures and materials) inside the asset bundles themselves, so it all just works. RE your concern of multiple-named files: an advantage of asset bundles is that they can share this information already. That's why the XML definitions might be useful to you. You could put all the shared data into its own bundle, and as long as this bundle is loaded, textures and other assets will be shared. There isn't any technical reason you couldn't load lots of models or whatever else at a time from the bundle. That would definitely be the simplest, most straight-forward way to handle shared dependencies though: throw all your stuff into one, single file.
  5. Did you use the code from Shadowmage's post? In that version, the file extension should be 'smf' (although you should probably choose your own extension and edit the code accordingly). You also don't need to use the KSP asset compiler, although it shouldn't be a problem if you do
  6. You don't put the .mu model into the bundle. You put the actual GameObject that you've set up (as a prefab) in there instead, bypassing the .mu creation step entirely. This asset bundle is essentially a mu file now, except it uses Unity's serialization instead of Squad's hand-rolled stuff that is missing lots of features. Everything else behaves exactly the same as if you were using a mu file -- define your cfg in exactly the same way. By the time the PartLoader is creating parts out of the loaded models, all of that stuff has been run and shaders are in-place.
  7. That method will work (for your shader problem). What exactly is the issue? The Shader.Find API apparently requires that the shaders exist at project build time, which is why they aren't found when the PartLoader is creating materials. That's why I suggest you bypass PartLoader entirely. If you put your shader AND model into an asset bundle and then use the previously posted code to load that as though it was a model, you won't be restricted to the extremely limited stuff PartLoader does. The shaders are all in squadcore.ksp. squadcorefx contains TextMeshPro shaders. You can inspect these using the Asset Bundle Browser, a free Unity asset you can download from their store
  8. You shouldn't even need a loader for the shader. Make sure it's stored inside the AssetBundle itself and Unity should do the rest
  9. I haven't been up with the latest stuff so I'm not sure if it's still functional, but the limitations of the mu loader are exactly why I loaded certain assets as asset bundles instead. @Shadowmage has a version posted, in fact: If you make sure your textures are included in the bundle, you shouldn't even need any post-loading stuff
  10. It doesn't matter for the Canvas' children, but it does for the Canvas itself. Also, the Canvas appears to copy the camera's transform and rotation which means the position and orientation of the Image you're adding are relevant. Here's a fixed version: private void Start() { GameObject canvasGO = new GameObject("censorcanvas"); canvasGO.layer = 0; // must be in camera's culling mask canvasGO.AddComponent<RectTransform>(); canvas = canvasGO.AddComponent<Canvas>(); canvas.renderMode = RenderMode.ScreenSpaceCamera; // UI elements do not show up if we use the local space camera Camera flightCam = FlightCamera.fetch.mainCamera; canvas.worldCamera = flightCam; canvas.planeDistance = flightCam.nearClipPlane + 0.5f; canvas.pixelPerfect = true; CanvasScaler canvasScalar = canvasGO.AddComponent<CanvasScaler>(); canvasScalar.uiScaleMode = CanvasScaler.ScaleMode.ConstantPixelSize; canvasGO.AddComponent<GraphicRaycaster>(); GameObject panelGO = new GameObject("censorbar"); panelGO.layer = LayerMask.NameToLayer("UI"); panel = panelGO.AddComponent<RectTransform>(); panelGO.transform.SetParent(canvasGO.transform, false); // gets moved Image censorbar = panelGO.AddComponent<Image>(); Texture2D tex = GameDatabase.Instance.GetTexture("Harm/harm", false); censorbar.sprite = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f)); panel.anchorMin = new Vector2(0, 0); panel.anchorMax = new Vector2(0, 0); panel.pivot = new Vector2(0.5f, 0.5f); panel.sizeDelta = new Vector2(tex.width, tex.height); } private void LateUpdate() { // this is the nearer of the two local space cameras Camera cam = FlightCamera.fetch.mainCamera; // make image track kerbal position on screen Vector2 screenPoint = (Vector2)cam.WorldToScreenPoint(transform.position); panel.anchoredPosition = screenPoint; // eventual goal is to also put image just in front of kerbal so it gets occluded by other closer objects // canvas.planeDistance = Vector3.Dot((transform.position - cam.transform.position), cam.transform.forward) - 0.5; canvas.planeDistance = Vector3.Distance(transform.position, cam.transform.position); }
  11. But why? Here's what I meant: private class JellyListener : MonoBehaviour { private JellyfishDishMk2 _jellydish; // your PartModule here private void Awake() { _jellydish = GetComponentInParent<JellyfishDishMk2>(); if (_jellydish == null) Debug.LogError("Didn't find expected Jellyfish PM"); } private void FirstAnimationFrame(UnityEngine.Object param) { var pm = param as JellyfishDishMk2; if (pm != null) pm.PlayStartingSound(); } // alternative method: note the lack of argument private void LastAnimationFrame() { _jellydish.PlayEndingSound(); } } // add events at beginning and end jellyfish.clip.AddEvent(new AnimationEvent() { time = 0f, functionName = "FirstAnimationFrame", objectReferenceParameter = this // note that any UnityEngine.Object works here: we could pass a sound, texture, ScriptableObject, MonoBehaviour, ... }); jellyfish.clip.AddEvent(new AnimationEvent() { time = jellyfish.length, functionName = "LastAnimationFrame" // note didn't use objectRef here, see listener method });
  12. While you could do that (any UnityEngine.Object or derived class ... like PartModule or MonoBehaviour), you don't need to. You already know exactly where your data is. You could cache the PartModule or wherever your data is being kept in whatever script you have as an event listener and call methods directly
  13. This kind of error can be a real PITA to debug, but in this case it's correct. The specific problem (at least, in my test bench of just downloading KAS 1.2 + current version of KOS) is that SaveUpgradePipeline.KAS.PatchFilesProcessor fails to load. This happens because it doesn't implement an abstract method. Between 1.6 and 1.7, UpgradeScript.OnUpgrade's signature changed. I didn't check that other mod you mentioned but I imagine something similar is going on. I don't think there's a 1.7 version of KAS yet or else this would be caught already
  14. Are you 100% sure it exported successfully? That looks like it might be the result of an incomplete model. Delete, re-export, then examine Unity console for any exceptions.
  15. You don't need ref or out here. Just use isDragging directly in the lambda functions; C# will create a closure over it and you effectively have a reference to it (although secretly the compiler creates a private inner class which is how it pulls this off). By the way, you probably don't want to be inserting triggers in OnUpdate(): every time the slider value changes, you add another set of triggers
  • Create New...