├── LICENSE ├── CHANGELOG.md ├── README.md └── MoreSuits.cs /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 x753 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v1.5.1 2 | - Deleted duplicate DLL file 3 | 4 | ## v1.5.0 5 | - Added jump audio support thanks to XuXiaolan 6 | 7 | ## v1.4.5 8 | - Added support for placing advanced.json files in the config folder (thanks darmuh) 9 | - .json files in the BepInEx/config/MoreSuits folder will be used in place of those in the advanced folder 10 | 11 | ## v1.4.4 12 | - Moved Default.png into the Advanced folder so it doesn't overwrite the base suit 13 | 14 | ## v1.4.3 15 | - Reverted a change to how suits were sorted 16 | 17 | ## v1.4.2 18 | - Fixed an issue where suits took up more memory than necessary 19 | - Added a config option to unlock all suits, so you won't have to buy them from the store 20 | 21 | ## v1.4.1 22 | - Fixed a bug when using LethalFashion 23 | - Added material support thanks to ViViKo 24 | 25 | ## v1.4.0 26 | - Bugfixes for adding suits to the shop 27 | - Added a config option (on by default) to position your suits closer together so you can have up to 20 fit on the rack 28 | - Added several new advanced options: "DISABLEKEYWORD", "SHADERPASS", "DISABLESHADERPASS", "SHADER" 29 | 30 | ## v1.3.3 31 | - Added a BepInEx config file so you can exclude certain suits from being loaded or ignore !less-suits.txt\ 32 | 33 | ## v1.3.0 34 | - More Suits can now be used as a library for other suit mods 35 | 36 |
Older Versions 37 | 38 | ## v1.2.1 39 | - Fixed suits being in a different order on the rack for each player 40 | 41 | ## v1.2.0 Suits in Rotating Shop 42 | - Added support for adding suits to the store rotation 43 | 44 | ## v1.1.0 More suits! 45 | - Added new suits by Graelyth and Curt 46 | - Added support for advanced features (normal maps, emission, etc) 47 | 48 | ## v1.0.0 Release 😎 49 | - Release 50 | 51 |
-------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # More Suits 2 | ### Adds more suits to choose from, and can be used as a library to load your own suits! 3 | 4 | ## Instructions 5 | Place the ```x753-More_Suits-X.X.X``` folder in your ```BepInEx/Plugins``` folder. Make sure the ```moresuits``` folder is in the same folder as ```MoreSuits.dll```. 6 | 7 | ## Config File 8 | After launching the game with the mod once, a config file is generated. In this file you can disable individual suits from being loaded, as well as ignore any ```!less-suits.txt``` file and attempt to load all suits (which is useful if you have another mod that helps manage lots of suits). 9 | 10 | ## Customize 11 | You can add .png files to the ```moresuits``` folder to add new suits as long as both the host and clients have the same files. 12 | 13 | ## Advanced 14 | You can add a .json file in the ```advanced``` folder with the same name as your .png file in the ```moresuits``` folder to enable additional features like emission. Place additional texture maps in the ```advanced``` folder. 15 | 16 | For a list of supported features, see: 17 | https://docs.unity3d.com/Packages/com.unity.render-pipelines.high-definition@14.0/manual/Lit-Shader.html 18 | 19 | ## Add Suits to Store 20 | Add a "PRICE" key to your advanced .json to put a suit in the store rotation. See ```Glow.json``` for an example of adding a suit with emission that must be purchased from the store. If you want to set a custom price for a suit from another mod in your modpack, create a new .json in the BepInEx/config/MoreSuitsConfig folder. 21 | 22 | ## Making your own More Suits mod 23 | Upload your own package with a ```BepInEx/plugins/moresuits``` folder in it (do not include the MoreSuits.dll file) and add ```x753-More_Suits-1.5.1``` as a dependency, and this mod will automatically load your .png files as suits. If you don't want some or all of the suits that originally come with my mod, adjust the config file ```BepInEx\config\x753.More_Suits.cfg```. Include a ```!less-suits.txt``` file in your ```moresuits``` folder to disable all the default suits that come with this mod. -------------------------------------------------------------------------------- /MoreSuits.cs: -------------------------------------------------------------------------------- 1 | using BepInEx; 2 | using BepInEx.Configuration; 3 | using HarmonyLib; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Reflection; 9 | using UnityEngine; 10 | 11 | namespace MoreSuits 12 | { 13 | [BepInPlugin(modGUID, modName, modVersion)] 14 | public class MoreSuitsMod : BaseUnityPlugin 15 | { 16 | private const string modGUID = "x753.More_Suits"; 17 | private const string modName = "More Suits"; 18 | private const string modVersion = "1.5.0"; 19 | 20 | private readonly Harmony harmony = new Harmony(modGUID); 21 | 22 | private static MoreSuitsMod Instance; 23 | 24 | public static bool SuitsAdded = false; 25 | 26 | public static string DisabledSuits; 27 | public static bool LoadAllSuits; 28 | public static bool MakeSuitsFitOnRack; 29 | public static bool UnlockAll; 30 | public static int MaxSuits; 31 | 32 | public static List customMaterials = new List(); 33 | public static List customAudioClips = new List(); 34 | 35 | private void Awake() 36 | { 37 | if (Instance == null) 38 | { 39 | Instance = this; 40 | } 41 | 42 | DisabledSuits = Config.Bind("General", "Disabled Suit List", "UglySuit751.png,UglySuit752.png,UglySuit753.png", "Comma-separated list of suits that shouldn't be loaded").Value; 43 | LoadAllSuits = Config.Bind("General", "Ignore !less-suits.txt", false, "If true, ignores the !less-suits.txt file and will attempt to load every suit, except those in the disabled list. This should be true if you're not worried about having too many suits.").Value; 44 | MakeSuitsFitOnRack = Config.Bind("General", "Make Suits Fit on Rack", true, "If true, squishes the suits together so more can fit on the rack.").Value; 45 | UnlockAll = Config.Bind("General", "Unlock All Suits", false, "If true, unlocks all custom suits that would normally be sold in the shop.").Value; 46 | MaxSuits = Config.Bind("General", "Max Suits", 100, "The maximum number of suits to load. If you have more, some will be ignored.").Value; 47 | 48 | harmony.PatchAll(); 49 | Logger.LogInfo($"Plugin {modName} is loaded!"); 50 | } 51 | 52 | [HarmonyPatch(typeof(StartOfRound))] 53 | internal class StartOfRoundPatch 54 | { 55 | [HarmonyPatch("Start")] 56 | [HarmonyPrefix] 57 | static void StartPatch(ref StartOfRound __instance) 58 | { 59 | try 60 | { 61 | if (!SuitsAdded) // we only need to add the new suits to the unlockables list once per game launch 62 | { 63 | int originalUnlockablesCount = __instance.unlockablesList.unlockables.Count; 64 | UnlockableItem originalSuit = new UnlockableItem(); 65 | 66 | int addedSuitCount = 0; 67 | for (int i = 0; i < __instance.unlockablesList.unlockables.Count; i++) 68 | { 69 | UnlockableItem unlockableItem = __instance.unlockablesList.unlockables[i]; 70 | 71 | if (unlockableItem.suitMaterial != null && unlockableItem.alreadyUnlocked) // find the default suit to use as a base 72 | { 73 | originalSuit = unlockableItem; 74 | 75 | // Get all .png files from all folders named moresuits in the BepInEx/plugins folder 76 | List suitsFolderPaths = Directory.GetDirectories(Paths.PluginPath, "moresuits", SearchOption.AllDirectories).ToList(); 77 | List texturePaths = new List(); 78 | List assetPaths = new List(); 79 | List disabledSuits = DisabledSuits.ToLower().Replace(".png", "").Split(',').ToList(); 80 | List disabledDefaultSuits = new List(); 81 | 82 | // Check through each moresuits folder for a text file called !less-suits.txt, which signals not to load any of the original suits that come with this mod 83 | if (!LoadAllSuits) 84 | { 85 | foreach (string suitsFolderPath in suitsFolderPaths) 86 | { 87 | if (File.Exists(Path.Combine(suitsFolderPath, "!less-suits.txt"))) 88 | { 89 | string[] defaultSuits = { "glow", "kirby", "knuckles", "luigi", "mario", "minion", "skeleton", "slayer", "smile" }; 90 | disabledDefaultSuits.AddRange(defaultSuits); // add every default suit in the mod to the disabled suits list 91 | break; 92 | } 93 | } 94 | } 95 | 96 | foreach (string suitsFolderPath in suitsFolderPaths) 97 | { 98 | if (suitsFolderPath != "") 99 | { 100 | string[] pngFiles = Directory.GetFiles(suitsFolderPath, "*.png"); 101 | string[] matBundles = Directory.GetFiles(suitsFolderPath, "*.matbundle", SearchOption.AllDirectories); // legacy bundle file extension 102 | string[] suitBundles = Directory.GetFiles(suitsFolderPath, "*.suitbundle", SearchOption.AllDirectories); 103 | 104 | texturePaths.AddRange(pngFiles); 105 | assetPaths.AddRange(matBundles); 106 | assetPaths.AddRange(suitBundles); 107 | } 108 | } 109 | 110 | assetPaths.Sort(); 111 | texturePaths.Sort(); 112 | 113 | //assetPaths = assetPaths.OrderBy(Path.GetFileNameWithoutExtension).ThenBy(p => p).ToList(); 114 | //texturePaths = texturePaths.OrderBy(Path.GetFileNameWithoutExtension).ThenBy(p => p).ToList(); 115 | 116 | Dictionary audioClips = new Dictionary(); 117 | 118 | try 119 | { 120 | foreach (string assetPath in assetPaths) 121 | { 122 | AssetBundle assetBundle = AssetBundle.LoadFromFile(assetPath); 123 | UnityEngine.Object[] assets = assetBundle.LoadAllAssets(); 124 | 125 | foreach (UnityEngine.Object asset in assets) 126 | { 127 | if (asset is Material) 128 | { 129 | Material material = (Material)asset; 130 | customMaterials.Add(material); 131 | } 132 | if (asset is AudioClip) 133 | { 134 | AudioClip audioClip = (AudioClip)asset; 135 | customAudioClips.Add(audioClip); 136 | } 137 | } 138 | } 139 | } 140 | catch (Exception ex) 141 | { 142 | Debug.Log("Something went wrong with More Suits! Could not load materials from asset bundle(s). Error: " + ex); 143 | } 144 | 145 | // Create new suits for each .png 146 | foreach (string texturePath in texturePaths) 147 | { 148 | // skip each suit that is in the disabled suits list 149 | if (disabledSuits.Contains(Path.GetFileNameWithoutExtension(texturePath).ToLower())) { continue; } 150 | string originalMoreSuitsPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); 151 | if (disabledDefaultSuits.Contains(Path.GetFileNameWithoutExtension(texturePath).ToLower()) && texturePath.Contains(originalMoreSuitsPath)) { continue; } 152 | 153 | UnlockableItem newSuit; 154 | Material newMaterial; 155 | 156 | if (Path.GetFileNameWithoutExtension(texturePath).ToLower() == "default") 157 | { 158 | newSuit = originalSuit; 159 | newMaterial = newSuit.suitMaterial; 160 | } 161 | else 162 | { 163 | // Serialize and deserialize to create a deep copy of the original suit item 164 | newSuit = JsonUtility.FromJson(JsonUtility.ToJson(originalSuit)); 165 | 166 | newMaterial = Instantiate(newSuit.suitMaterial); 167 | } 168 | 169 | byte[] fileData = File.ReadAllBytes(texturePath); 170 | Texture2D texture = new Texture2D(2, 2); 171 | texture.LoadImage(fileData); 172 | 173 | texture.Apply(true, true); 174 | 175 | newMaterial.mainTexture = texture; 176 | 177 | newSuit.unlockableName = Path.GetFileNameWithoutExtension(texturePath); 178 | 179 | // Optional modification of other properties like normal maps, emission, etc 180 | // https://docs.unity3d.com/Packages/com.unity.render-pipelines.high-definition@14.0/manual/Lit-Shader.html 181 | try 182 | { 183 | string advancedJsonPath = Path.Combine(Path.GetDirectoryName(texturePath), "advanced", newSuit.unlockableName + ".json"); 184 | string configJsonPath = Path.Combine(Path.GetDirectoryName(Paths.ConfigPath), "config\\MoreSuitsConfig", newSuit.unlockableName + ".json"); 185 | 186 | if (File.Exists(configJsonPath)) 187 | { 188 | Instance.Logger.LogInfo($"Utilizing [ {configJsonPath} ] for suit - {newSuit.unlockableName}!"); 189 | advancedJsonPath = configJsonPath; 190 | } 191 | 192 | if (File.Exists(advancedJsonPath)) 193 | { 194 | string[] lines = File.ReadAllLines(advancedJsonPath); 195 | 196 | foreach (string line in lines) 197 | { 198 | string[] keyValue = line.Trim().Split(':'); 199 | if (keyValue.Length == 2) 200 | { 201 | string keyData = keyValue[0].Trim('"', ' ', ','); 202 | string valueData = keyValue[1].Trim('"', ' ', ','); 203 | 204 | if (valueData.Contains(".png")) 205 | { 206 | string advancedTexturePath = Path.Combine(Path.GetDirectoryName(texturePath), "advanced", valueData); 207 | byte[] advancedTextureData = File.ReadAllBytes(advancedTexturePath); 208 | Texture2D advancedTexture = new Texture2D(2, 2); 209 | advancedTexture.LoadImage(advancedTextureData); 210 | 211 | advancedTexture.Apply(true, true); 212 | 213 | newMaterial.SetTexture(keyData, advancedTexture); 214 | } 215 | else if (keyData == "PRICE" && int.TryParse(valueData, out int intValue)) // If the advanced json has a price, set it up so it rotates into the shop 216 | { 217 | try 218 | { 219 | if(!UnlockAll) 220 | newSuit = AddToRotatingShop(newSuit, intValue, __instance.unlockablesList.unlockables.Count); 221 | } 222 | catch (Exception ex) 223 | { 224 | Debug.Log("Something went wrong with More Suits! Could not add a suit to the rotating shop. Error: " + ex); 225 | } 226 | } 227 | else if (valueData == "KEYWORD") 228 | { 229 | newMaterial.EnableKeyword(keyData); 230 | } 231 | else if (valueData == "DISABLEKEYWORD") 232 | { 233 | newMaterial.DisableKeyword(keyData); 234 | } 235 | else if (valueData == "SHADERPASS") 236 | { 237 | newMaterial.SetShaderPassEnabled(keyData, true); 238 | } 239 | else if (valueData == "DISABLESHADERPASS") 240 | { 241 | newMaterial.SetShaderPassEnabled(keyData, false); 242 | } 243 | else if (keyData == "SHADER") 244 | { 245 | Shader newShader = Shader.Find(valueData); 246 | newMaterial.shader = newShader; 247 | } 248 | else if (keyData == "MATERIAL") 249 | { 250 | foreach (Material material in customMaterials) 251 | { 252 | if (material.name == valueData) 253 | { 254 | newMaterial = Instantiate(material); 255 | newMaterial.mainTexture = texture; 256 | break; 257 | } 258 | } 259 | } 260 | else if (keyData == "AUDIOCLIP") 261 | { 262 | foreach (AudioClip audioClip in customAudioClips) 263 | { 264 | if (audioClip.name == valueData) 265 | { 266 | newSuit.jumpAudio = audioClip; 267 | break; 268 | } 269 | } 270 | } 271 | else if (float.TryParse(valueData, out float floatValue)) 272 | { 273 | newMaterial.SetFloat(keyData, floatValue); 274 | } 275 | else if (TryParseVector4(valueData, out Vector4 vectorValue)) 276 | { 277 | newMaterial.SetVector(keyData, vectorValue); 278 | } 279 | } 280 | } 281 | } 282 | } 283 | catch (Exception ex) 284 | { 285 | Debug.Log("Something went wrong with More Suits! Error: " + ex); 286 | } 287 | 288 | newSuit.suitMaterial = newMaterial; 289 | 290 | if (newSuit.unlockableName.ToLower() != "default") 291 | { 292 | if (addedSuitCount == MaxSuits) 293 | { 294 | Debug.Log("Attempted to add a suit, but you've already reached the max number of suits! Modify the config if you want more."); 295 | } 296 | else 297 | { 298 | __instance.unlockablesList.unlockables.Add(newSuit); 299 | addedSuitCount++; 300 | } 301 | } 302 | } 303 | 304 | SuitsAdded = true; 305 | break; 306 | } 307 | } 308 | 309 | UnlockableItem dummySuit = JsonUtility.FromJson(JsonUtility.ToJson(originalSuit)); 310 | dummySuit.alreadyUnlocked = false; 311 | dummySuit.hasBeenMoved = false; 312 | dummySuit.placedPosition = Vector3.zero; 313 | dummySuit.placedRotation = Vector3.zero; 314 | dummySuit.unlockableType = 753; // this unlockable type is not used 315 | while (__instance.unlockablesList.unlockables.Count < originalUnlockablesCount + MaxSuits) 316 | { 317 | __instance.unlockablesList.unlockables.Add(dummySuit); 318 | } 319 | } 320 | } 321 | catch (Exception ex) 322 | { 323 | Debug.Log("Something went wrong with More Suits! Error: " + ex); 324 | } 325 | 326 | } 327 | 328 | [HarmonyPatch("PositionSuitsOnRack")] 329 | [HarmonyPrefix] 330 | static bool PositionSuitsOnRackPatch(ref StartOfRound __instance) 331 | { 332 | List suits = UnityEngine.Object.FindObjectsOfType().ToList(); 333 | suits = suits.OrderBy(suit => suit.syncedSuitID.Value).ToList(); 334 | int index = 0; 335 | foreach (UnlockableSuit suit in suits) 336 | { 337 | AutoParentToShip component = suit.gameObject.GetComponent(); 338 | component.overrideOffset = true; 339 | 340 | float offsetModifier = 0.18f; 341 | if (MakeSuitsFitOnRack && suits.Count > 13) 342 | { 343 | offsetModifier = offsetModifier / (Math.Min(suits.Count, 20) / 12f); // squish the suits together to make them all fit 344 | } 345 | 346 | component.positionOffset = new Vector3(-2.45f, 2.75f, -8.41f) + __instance.rightmostSuitPosition.forward * offsetModifier * (float)index; 347 | component.rotationOffset = new Vector3(0f, 90f, 0f); 348 | 349 | index++; 350 | } 351 | 352 | return false; // don't run the original 353 | } 354 | } 355 | 356 | private static TerminalNode cancelPurchase; 357 | private static TerminalKeyword buyKeyword; 358 | private static UnlockableItem AddToRotatingShop(UnlockableItem newSuit, int price, int unlockableID) 359 | { 360 | Terminal terminal = UnityEngine.Object.FindObjectOfType(); 361 | for (int i = 0; i < terminal.terminalNodes.allKeywords.Length; i++) 362 | { 363 | if (terminal.terminalNodes.allKeywords[i].name == "Buy") 364 | { 365 | buyKeyword = terminal.terminalNodes.allKeywords[i]; 366 | break; 367 | } 368 | } 369 | 370 | newSuit.alreadyUnlocked = false; 371 | newSuit.hasBeenMoved = false; 372 | newSuit.placedPosition = Vector3.zero; 373 | newSuit.placedRotation = Vector3.zero; 374 | 375 | newSuit.shopSelectionNode = ScriptableObject.CreateInstance(); 376 | newSuit.shopSelectionNode.name = newSuit.unlockableName + "SuitBuy1"; 377 | newSuit.shopSelectionNode.creatureName = newSuit.unlockableName + " suit"; 378 | newSuit.shopSelectionNode.displayText = "You have requested to order " + newSuit.unlockableName + " suits.\nTotal cost of item: [totalCost].\n\nPlease CONFIRM or DENY.\n\n"; 379 | newSuit.shopSelectionNode.clearPreviousText = true; 380 | newSuit.shopSelectionNode.shipUnlockableID = unlockableID; 381 | newSuit.shopSelectionNode.itemCost = price; 382 | newSuit.shopSelectionNode.overrideOptions = true; 383 | 384 | CompatibleNoun confirm = new CompatibleNoun(); 385 | confirm.noun = ScriptableObject.CreateInstance(); 386 | confirm.noun.word = "confirm"; 387 | confirm.noun.isVerb = true; 388 | 389 | confirm.result = ScriptableObject.CreateInstance(); 390 | confirm.result.name = newSuit.unlockableName + "SuitBuyConfirm"; 391 | confirm.result.creatureName = ""; 392 | confirm.result.displayText = "Ordered " + newSuit.unlockableName + " suits! Your new balance is [playerCredits].\n\n"; 393 | confirm.result.clearPreviousText = true; 394 | confirm.result.shipUnlockableID = unlockableID; 395 | confirm.result.buyUnlockable = true; 396 | confirm.result.itemCost = price; 397 | confirm.result.terminalEvent = ""; 398 | 399 | CompatibleNoun deny = new CompatibleNoun(); 400 | deny.noun = ScriptableObject.CreateInstance(); 401 | deny.noun.word = "deny"; 402 | deny.noun.isVerb = true; 403 | 404 | if (cancelPurchase == null) 405 | { 406 | cancelPurchase = ScriptableObject.CreateInstance(); // we can use the same Cancel Purchase node 407 | } 408 | deny.result = cancelPurchase; 409 | deny.result.name = "MoreSuitsCancelPurchase"; 410 | deny.result.displayText = "Cancelled order.\n"; 411 | 412 | newSuit.shopSelectionNode.terminalOptions = new CompatibleNoun[] { confirm, deny }; 413 | 414 | TerminalKeyword suitKeyword = ScriptableObject.CreateInstance(); 415 | suitKeyword.name = newSuit.unlockableName + "Suit"; 416 | suitKeyword.word = newSuit.unlockableName.ToLower() + " suit"; 417 | suitKeyword.defaultVerb = buyKeyword; 418 | 419 | CompatibleNoun suitCompatibleNoun = new CompatibleNoun(); 420 | suitCompatibleNoun.noun = suitKeyword; 421 | suitCompatibleNoun.result = newSuit.shopSelectionNode; 422 | List buyKeywordList = buyKeyword.compatibleNouns.ToList(); 423 | buyKeywordList.Add(suitCompatibleNoun); 424 | buyKeyword.compatibleNouns = buyKeywordList.ToArray(); 425 | 426 | List allKeywordsList = terminal.terminalNodes.allKeywords.ToList(); 427 | allKeywordsList.Add(suitKeyword); 428 | allKeywordsList.Add(confirm.noun); 429 | allKeywordsList.Add(deny.noun); 430 | terminal.terminalNodes.allKeywords = allKeywordsList.ToArray(); 431 | 432 | return newSuit; 433 | } 434 | 435 | public static bool TryParseVector4(string input, out Vector4 vector) 436 | { 437 | vector = Vector4.zero; 438 | 439 | string[] components = input.Split(','); 440 | 441 | if (components.Length == 4) 442 | { 443 | if (float.TryParse(components[0], out float x) && 444 | float.TryParse(components[1], out float y) && 445 | float.TryParse(components[2], out float z) && 446 | float.TryParse(components[3], out float w)) 447 | { 448 | vector = new Vector4(x, y, z, w); 449 | return true; 450 | } 451 | } 452 | 453 | return false; 454 | } 455 | } 456 | } --------------------------------------------------------------------------------