├── .gitignore ├── LICENSE ├── README.md ├── TagManager.asset ├── WorkbenchEditorScripts ├── EnumPicker.cs ├── Extensions.cs ├── Stubs.cs └── TNH_EncryptionSpawnPoint.cs ├── WurstMod.sln ├── WurstMod ├── FileIDUtil.cs ├── MappingComponents │ ├── ComponentProxy.cs │ ├── Generic │ │ ├── AICoverPoint.cs │ │ ├── AnvilPrefab.cs │ │ ├── CustomScene.cs │ │ ├── FVRHandGrabPoint.cs │ │ ├── FVRReverbEnvironment.cs │ │ ├── HandTrigger.cs │ │ ├── ItemSpawner.cs │ │ ├── PMat.cs │ │ ├── PlayerTrigger.cs │ │ ├── SosigSpawner.cs │ │ ├── Target.cs │ │ └── Trigger.cs │ ├── Sandbox │ │ ├── GenericPrefab.cs │ │ ├── GroundPanel.cs │ │ ├── PointableButton.cs │ │ └── Spawn.cs │ └── TakeAndHold │ │ ├── AttackVector.cs │ │ ├── ForcedSpawn.cs │ │ ├── Respawn.cs │ │ ├── Scoreboard.cs │ │ ├── ScoreboardArea.cs │ │ ├── TNH_DestructibleBarrierPoint.cs │ │ ├── TNH_HoldPoint.cs │ │ └── TNH_SupplyPoint.cs ├── Properties │ └── AssemblyInfo.cs ├── Runtime │ ├── CustomLevelFinder.cs │ ├── Entrypoint.cs │ ├── LegacySupport.cs │ ├── Loader.cs │ ├── ModdedLevelInfo.cs │ ├── ObjectReferences.cs │ ├── Patches.cs │ ├── SceneLoaders │ │ ├── CustomSceneLoader.cs │ │ ├── SandboxSceneLoader.cs │ │ └── TakeAndHoldSceneLoader.cs │ ├── ScenePatchers │ │ ├── Generic_LevelPopulator.cs │ │ └── TNH_LevelSelector.cs │ ├── SosigSpawnerHelper.cs │ └── SpriteLoader.cs ├── Shared │ ├── Constants.cs │ ├── Enums.cs │ ├── Extensions.cs │ ├── LevelInfo.cs │ └── ResourceDefs.cs ├── UnityEditor │ ├── Exporter.cs │ ├── ExporterWindow.cs │ └── SceneExporters │ │ ├── SandboxExporter.cs │ │ ├── SceneExporter.cs │ │ └── TakeAndHoldExporter.cs ├── WurstMod.csproj ├── legacyManifest.json ├── manifest.json └── packages.config ├── WurstModCodeGen ├── App.config ├── Extensions.cs ├── Generator.cs ├── Properties │ └── AssemblyInfo.cs └── WurstModCodeGen.csproj ├── WurstModWorkbench.unitypackage ├── nuget.config └── pdb2mdb.exe /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Koba 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WurstMod 2 | WurstMod is a custom level loader for the popular VR game _Hotdogs, Horseshoes & Handgrenades_. 3 | With out of the box support for both sandbox and Take & Hold levels, plus an easy to use API for creating custom levels and game modes. 4 | 5 | ## Compatibility notes 6 | * As of `2.2.0` WurstMod requires H3VR build `7036061` or later. You likely have this already. 7 | * WurstMod `2.1.1` will work on older versions of the game going back fairly far 8 | * Maps made `1.x` and `2.x` will currently work on any `2.x`. 9 | 10 | ## Quickstart Links 11 | - [Installing WurstMod and custom levels](https://h3vrmodding.miraheze.org/wiki/WurstMod) 12 | - [Creating Custom Maps](https://github.com/Nolenz/WurstMod/wiki/Setting-up-your-environment) 13 | - [Developer Reference](https://github.com/Nolenz/WurstMod/wiki/Developer-References) -------------------------------------------------------------------------------- /TagManager.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!78 &1 4 | TagManager: 5 | serializedVersion: 2 6 | tags: [] 7 | layers: 8 | - Default 9 | - TransparentFX 10 | - Ignore Raycast 11 | - 12 | - Water 13 | - UI 14 | - 15 | - 16 | - NoCol 17 | - HandTrigger 18 | - Interactable 19 | - NoSelfCol 20 | - NavBlock 21 | - AgentMeleeWeapon 22 | - NonBlockingSmoke 23 | - PlayerHead 24 | - ColOnlyHead 25 | - TurnMeIntoSomethingElse2 26 | - ThermalContact 27 | - Environment 28 | - AgentBody 29 | - TeleValid 30 | - LightExclusion 31 | - PlayerAndRBTrigger 32 | - Smoke 33 | - AIEntity 34 | - 35 | - 36 | - ExternalCamOnly 37 | - Distant 38 | - Burning 39 | - Flammable 40 | m_SortingLayers: 41 | - name: Default 42 | uniqueID: 0 43 | locked: 0 44 | -------------------------------------------------------------------------------- /WorkbenchEditorScripts/EnumPicker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using UnityEngine; 4 | using System.Collections; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | // Adapted from u/kalin_r 9 | // https://gist.github.com/kalineh/ad5135946f2009c36f755eea0a880998 10 | 11 | #if UNITY_EDITOR 12 | using UnityEditor; 13 | 14 | public class EnumPickerWindow : EditorWindow 15 | { 16 | private static GUIStyle regularStyle; 17 | private static GUIStyle selectedStyle; 18 | 19 | private string enumName; 20 | private List valuesRaw; 21 | private List valuesFiltered; 22 | private string filter; 23 | 24 | private EditorWindow parent; 25 | private Vector2 scroll; 26 | 27 | private System.Action onSelectCallback; 28 | 29 | public void ShowCustom(string name, List values, Rect rect, System.Action onSelect) 30 | { 31 | regularStyle = new GUIStyle(EditorStyles.label); 32 | regularStyle.active = regularStyle.normal; 33 | 34 | selectedStyle = new GUIStyle(EditorStyles.label); 35 | selectedStyle.normal = selectedStyle.focused; 36 | selectedStyle.active = selectedStyle.focused; 37 | 38 | enumName = name; 39 | valuesRaw = new List(values); 40 | valuesFiltered = new List(values); 41 | filter = ""; 42 | onSelectCallback = onSelect; 43 | 44 | parent = focusedWindow; 45 | 46 | var screenRect = rect; 47 | var screenSize = new Vector2(400, 400); 48 | 49 | screenRect.position = GUIUtility.GUIToScreenPoint(screenRect.position); 50 | 51 | ShowAsDropDown(screenRect, screenSize); 52 | Focus(); 53 | 54 | GUI.FocusControl("filter"); 55 | } 56 | 57 | private void OnGUI() 58 | { 59 | GUILayout.Label(string.Format("Enum Type: {0}", enumName)); 60 | 61 | GUI.SetNextControlName("filter"); 62 | var filterUpdate = GUILayout.TextField(filter); 63 | if (filterUpdate != filter) 64 | FilterValues(filterUpdate); 65 | 66 | // always focused 67 | GUI.FocusControl("filter"); 68 | 69 | scroll = GUILayout.BeginScrollView(scroll); 70 | 71 | for (int i = 0; i < valuesFiltered.Count; ++i) 72 | { 73 | var value = valuesFiltered[i]; 74 | var style = i == 0 ? selectedStyle : regularStyle; 75 | var rect = GUILayoutUtility.GetRect(new GUIContent(value), style); 76 | 77 | var clicked = GUI.Button(rect, value); 78 | if (clicked) 79 | { 80 | GUILayout.EndScrollView(); 81 | 82 | onSelectCallback(value); 83 | Close(); 84 | parent.Repaint(); 85 | parent.Focus(); 86 | 87 | return; 88 | } 89 | } 90 | 91 | GUILayout.EndScrollView(); 92 | 93 | if (Event.current.type == EventType.KeyDown && Event.current.keyCode == KeyCode.Return) 94 | { 95 | if (valuesFiltered.Count > 0) 96 | onSelectCallback(valuesFiltered[0]); 97 | 98 | Close(); 99 | parent.Repaint(); 100 | parent.Focus(); 101 | } 102 | 103 | if (Event.current.type == EventType.KeyDown && Event.current.keyCode == KeyCode.Escape) 104 | { 105 | Close(); 106 | parent.Repaint(); 107 | parent.Focus(); 108 | } 109 | } 110 | 111 | public void OnLostFocus() 112 | { 113 | Close(); 114 | } 115 | 116 | private void FilterValues(string filterUpdate) 117 | { 118 | filter = filterUpdate; 119 | 120 | var filterLower = filter.ToLower(); 121 | 122 | valuesFiltered.Clear(); 123 | 124 | for (int i = 0; i < valuesRaw.Count; ++i) 125 | { 126 | var value = valuesRaw[i]; 127 | var lower = value.ToLower(); 128 | if (lower.Contains(filterLower)) 129 | valuesFiltered.Add(value); 130 | } 131 | } 132 | } 133 | 134 | [CustomPropertyDrawer(typeof(WurstMod.Shared.ResourceDefs.AnvilAsset))] 135 | public class EnumDrawers : EnumPicker 136 | { 137 | public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) 138 | { 139 | base.OnGUI(position, property, label); 140 | } 141 | } 142 | 143 | public class EnumPicker : PropertyDrawer 144 | { 145 | private EnumPickerWindow window; 146 | 147 | public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) 148 | { 149 | var valuesRaw = Enum.GetValues(fieldInfo.FieldType); 150 | if (valuesRaw.Length <= 0) 151 | return; 152 | 153 | var valuesStr = new List(); 154 | for (int i = 0; i < valuesRaw.Length; ++i) 155 | { 156 | var raw = valuesRaw.GetValue(i); 157 | var str = raw.ToString(); 158 | 159 | valuesStr.Add(str); 160 | } 161 | 162 | var enumName = fieldInfo.FieldType.Name; 163 | var currentName = Enum.GetName(fieldInfo.FieldType, property.intValue); 164 | 165 | EditorGUI.PrefixLabel(position, label); 166 | 167 | GUI.SetNextControlName(property.propertyPath); 168 | 169 | var fieldRect = new Rect(position.x + EditorGUIUtility.labelWidth, position.y, position.width - EditorGUIUtility.labelWidth, position.height); 170 | 171 | if (GUI.Button(fieldRect, currentName, EditorStyles.popup)) 172 | { 173 | window = EditorWindow.GetWindow(); 174 | 175 | System.Action callback = str => 176 | { 177 | var index = (int)Convert.ChangeType(Enum.Parse(fieldInfo.FieldType, str), fieldInfo.FieldType); 178 | 179 | property.serializedObject.Update(); 180 | property.intValue = index; 181 | property.serializedObject.ApplyModifiedProperties(); 182 | }; 183 | 184 | window.ShowCustom(enumName, valuesStr, fieldRect, callback); 185 | window.Focus(); 186 | } 187 | } 188 | } 189 | #endif 190 | -------------------------------------------------------------------------------- /WorkbenchEditorScripts/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Reflection; 6 | using UnityEngine; 7 | 8 | #if UNITY_EDITOR 9 | using UnityEditor; 10 | #endif 11 | 12 | public static class Extensions 13 | { 14 | public static Type[] GetTypesSafe(this Assembly asm) 15 | { 16 | Type[] retval; 17 | try 18 | { 19 | retval = asm.GetTypes(); 20 | } 21 | catch (ReflectionTypeLoadException e) 22 | { 23 | return e.Types.Where(t => t != null).ToArray(); 24 | } 25 | return retval; 26 | } 27 | 28 | 29 | #if UNITY_EDITOR 30 | public static List GetProperties(this Editor e) 31 | { 32 | List result = new List(); 33 | SerializedProperty iter = e.serializedObject.GetIterator(); 34 | if (iter.NextVisible(true)) 35 | { 36 | do 37 | { 38 | result.Add(iter.Copy()); 39 | } 40 | while (iter.NextVisible(false)); 41 | } 42 | 43 | return result; 44 | } 45 | #endif 46 | } 47 | -------------------------------------------------------------------------------- /WorkbenchEditorScripts/Stubs.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | 5 | // Old Unity forces its DLLs to be called Assembly-CSharp 6 | // WurstMod references Assembly-CSharp, H3VR's assembly 7 | // Unity implicitly creates an Assembly-CSharp for every project 8 | // Therefore, Unity thinks it should be able to load H3 classes from Assembly-CSharp... 9 | // Stub out everything we use to silence errors. 10 | 11 | namespace FistVR 12 | { 13 | public class TNH_HoldPoint { } 14 | public class NavMeshLinkExtension { } 15 | public class TNH_Manager { } 16 | public class MainMenuScenePointable { } 17 | public class FVRViveHand { } 18 | public class TNH_PointSequence { } 19 | public class TNH_SafePositionMatrix { } 20 | public class TNH_SupplyPoint { } 21 | public class Sosig { } 22 | public class SosigEnemyTemplate { } 23 | public class SosigWeapon { } 24 | public class FVRObject { } 25 | public class SosigConfigTemplate { } 26 | public class SosigOutfitConfig { } 27 | public class SosigLink { } 28 | } 29 | -------------------------------------------------------------------------------- /WorkbenchEditorScripts/TNH_EncryptionSpawnPoint.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace FistVR 4 | { 5 | public class TNH_EncryptionSpawnPoint : MonoBehaviour 6 | { 7 | public bool[] AllowedSpawns = new bool[11]; 8 | } 9 | } -------------------------------------------------------------------------------- /WurstMod.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29911.84 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WurstMod", "WurstMod\WurstMod.csproj", "{43E34E28-2C7E-42F4-92CA-C731AECFF593}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WurstModCodeGen", "WurstModCodeGen\WurstModCodeGen.csproj", "{3291AD19-A7C7-497F-BFFA-2D4F93D3F50F}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {43E34E28-2C7E-42F4-92CA-C731AECFF593}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {43E34E28-2C7E-42F4-92CA-C731AECFF593}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {43E34E28-2C7E-42F4-92CA-C731AECFF593}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {43E34E28-2C7E-42F4-92CA-C731AECFF593}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {3291AD19-A7C7-497F-BFFA-2D4F93D3F50F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {3291AD19-A7C7-497F-BFFA-2D4F93D3F50F}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {3291AD19-A7C7-497F-BFFA-2D4F93D3F50F}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {3291AD19-A7C7-497F-BFFA-2D4F93D3F50F}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {53478FAB-56EB-4427-944C-B1D66FFEED9D} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /WurstMod/FileIDUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Security.Cryptography; 5 | using System.Text; 6 | 7 | namespace WurstMod 8 | { 9 | // From "Lambda Knight" on the Unity forums. 10 | // https://forum.unity.com/threads/yaml-fileid-hash-function-for-dll-scripts.252075/#post-1695479 11 | class FileIDUtil 12 | { 13 | public static int Compute(string typeNamespace, string typeName) 14 | { 15 | string toBeHashed = "s\0\0\0" + typeNamespace + typeName; 16 | 17 | using (HashAlgorithm hash = new MD4()) 18 | { 19 | byte[] hashed = hash.ComputeHash(System.Text.Encoding.UTF8.GetBytes(toBeHashed)); 20 | 21 | int result = 0; 22 | 23 | for (int i = 3; i >= 0; --i) 24 | { 25 | result <<= 8; 26 | result |= hashed[i]; 27 | } 28 | 29 | return result; 30 | } 31 | } 32 | } 33 | 34 | public class MD4 : HashAlgorithm 35 | { 36 | private uint _a; 37 | private uint _b; 38 | private uint _c; 39 | private uint _d; 40 | private uint[] _x; 41 | private int _bytesProcessed; 42 | 43 | public MD4() 44 | { 45 | _x = new uint[16]; 46 | 47 | Initialize(); 48 | } 49 | 50 | public override void Initialize() 51 | { 52 | _a = 0x67452301; 53 | _b = 0xefcdab89; 54 | _c = 0x98badcfe; 55 | _d = 0x10325476; 56 | 57 | _bytesProcessed = 0; 58 | } 59 | 60 | protected override void HashCore(byte[] array, int offset, int length) 61 | { 62 | ProcessMessage(Bytes(array, offset, length)); 63 | } 64 | 65 | protected override byte[] HashFinal() 66 | { 67 | try 68 | { 69 | ProcessMessage(Padding()); 70 | 71 | return new[] { _a, _b, _c, _d }.SelectMany(word => Bytes(word)).ToArray(); 72 | } 73 | finally 74 | { 75 | Initialize(); 76 | } 77 | } 78 | 79 | private void ProcessMessage(IEnumerable bytes) 80 | { 81 | foreach (byte b in bytes) 82 | { 83 | int c = _bytesProcessed & 63; 84 | int i = c >> 2; 85 | int s = (c & 3) << 3; 86 | 87 | _x[i] = (_x[i] & ~((uint)255 << s)) | ((uint)b << s); 88 | 89 | if (c == 63) 90 | { 91 | Process16WordBlock(); 92 | } 93 | 94 | _bytesProcessed++; 95 | } 96 | } 97 | 98 | private static IEnumerable Bytes(byte[] bytes, int offset, int length) 99 | { 100 | for (int i = offset; i < length; i++) 101 | { 102 | yield return bytes[i]; 103 | } 104 | } 105 | 106 | private IEnumerable Bytes(uint word) 107 | { 108 | yield return (byte)(word & 255); 109 | yield return (byte)((word >> 8) & 255); 110 | yield return (byte)((word >> 16) & 255); 111 | yield return (byte)((word >> 24) & 255); 112 | } 113 | 114 | private IEnumerable Repeat(byte value, int count) 115 | { 116 | for (int i = 0; i < count; i++) 117 | { 118 | yield return value; 119 | } 120 | } 121 | 122 | private IEnumerable Padding() 123 | { 124 | return Repeat(128, 1) 125 | .Concat(Repeat(0, ((_bytesProcessed + 8) & 0x7fffffc0) + 55 - _bytesProcessed)) 126 | .Concat(Bytes((uint)_bytesProcessed << 3)) 127 | .Concat(Repeat(0, 4)); 128 | } 129 | 130 | private void Process16WordBlock() 131 | { 132 | uint aa = _a; 133 | uint bb = _b; 134 | uint cc = _c; 135 | uint dd = _d; 136 | 137 | foreach (int k in new[] { 0, 4, 8, 12 }) 138 | { 139 | aa = Round1Operation(aa, bb, cc, dd, _x[k], 3); 140 | dd = Round1Operation(dd, aa, bb, cc, _x[k + 1], 7); 141 | cc = Round1Operation(cc, dd, aa, bb, _x[k + 2], 11); 142 | bb = Round1Operation(bb, cc, dd, aa, _x[k + 3], 19); 143 | } 144 | 145 | foreach (int k in new[] { 0, 1, 2, 3 }) 146 | { 147 | aa = Round2Operation(aa, bb, cc, dd, _x[k], 3); 148 | dd = Round2Operation(dd, aa, bb, cc, _x[k + 4], 5); 149 | cc = Round2Operation(cc, dd, aa, bb, _x[k + 8], 9); 150 | bb = Round2Operation(bb, cc, dd, aa, _x[k + 12], 13); 151 | } 152 | 153 | foreach (int k in new[] { 0, 2, 1, 3 }) 154 | { 155 | aa = Round3Operation(aa, bb, cc, dd, _x[k], 3); 156 | dd = Round3Operation(dd, aa, bb, cc, _x[k + 8], 9); 157 | cc = Round3Operation(cc, dd, aa, bb, _x[k + 4], 11); 158 | bb = Round3Operation(bb, cc, dd, aa, _x[k + 12], 15); 159 | } 160 | 161 | unchecked 162 | { 163 | _a += aa; 164 | _b += bb; 165 | _c += cc; 166 | _d += dd; 167 | } 168 | } 169 | 170 | private static uint ROL(uint value, int numberOfBits) 171 | { 172 | return (value << numberOfBits) | (value >> (32 - numberOfBits)); 173 | } 174 | 175 | private static uint Round1Operation(uint a, uint b, uint c, uint d, uint xk, int s) 176 | { 177 | unchecked 178 | { 179 | return ROL(a + ((b & c) | (~b & d)) + xk, s); 180 | } 181 | } 182 | 183 | private static uint Round2Operation(uint a, uint b, uint c, uint d, uint xk, int s) 184 | { 185 | unchecked 186 | { 187 | return ROL(a + ((b & c) | (b & d) | (c & d)) + xk + 0x5a827999, s); 188 | } 189 | } 190 | 191 | private static uint Round3Operation(uint a, uint b, uint c, uint d, uint xk, int s) 192 | { 193 | unchecked 194 | { 195 | return ROL(a + (b ^ c ^ d) + xk + 0x6ed9eba1, s); 196 | } 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /WurstMod/MappingComponents/ComponentProxy.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | #if UNITY_EDITOR 4 | using WurstMod.UnityEditor; 5 | #endif 6 | 7 | namespace WurstMod.MappingComponents 8 | { 9 | public abstract class ComponentProxy : MonoBehaviour 10 | { 11 | /// 12 | /// Certain components MUST be initialized before others. Override this 13 | /// to ensure ordering. HIGH values run before lower values, default importance is 1. 14 | /// 15 | public virtual int ResolveOrder => 1; 16 | 17 | /// 18 | /// This is called at run-time to resolve the proxied component. 19 | /// Returns true if the unproxied component should be deleted afterwards. 20 | /// 21 | public virtual void InitializeComponent() { } 22 | 23 | #if UNITY_EDITOR 24 | /// 25 | /// This is called before the map is exported. Put one-time calculations and component validations here. 26 | /// 27 | public virtual void OnExport(ExportErrors err) { } 28 | #endif 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /WurstMod/MappingComponents/Generic/AICoverPoint.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace WurstMod.MappingComponents.Generic 4 | { 5 | /// 6 | /// AI take cover here. Place on floor, rotation doesn't matter. 7 | /// 8 | public class AICoverPoint : ComponentProxy 9 | { 10 | private void OnDrawGizmos() 11 | { 12 | Gizmos.color = new Color(0.0f, 0.6f, 0.0f, 0.5f); 13 | Gizmos.matrix = transform.localToWorldMatrix; 14 | Gizmos.DrawSphere(Vector3.zero, 0.1f); 15 | } 16 | 17 | public override int ResolveOrder => 3; 18 | 19 | public override void InitializeComponent() 20 | { 21 | global::AICoverPoint real = gameObject.AddComponent(); 22 | 23 | // These seem to be constant, and Calc and CalcNew are an enigma. 24 | real.Heights = new [] { 3f, 0.5f, 1.1f, 1.5f }; 25 | real.Calc(); 26 | 27 | Destroy(this); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /WurstMod/MappingComponents/Generic/AnvilPrefab.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FistVR; 3 | using UnityEngine; 4 | using WurstMod.Runtime; 5 | using WurstMod.Shared; 6 | 7 | namespace WurstMod.MappingComponents.Generic 8 | { 9 | [Obsolete] 10 | [AddComponentMenu("")] 11 | public class AnvilPrefab : ComponentProxy 12 | { 13 | // Inspector 14 | // Using the WeaponStuff enum covers most of the stuff people probably want to instantiate, 15 | // but not all of it. TODO reconsider this implementation. 16 | public ResourceDefs.AnvilAsset prefab; 17 | public bool spawnOnSceneLoad = false; 18 | 19 | public override void InitializeComponent() 20 | { 21 | if (spawnOnSceneLoad) Spawn(); 22 | } 23 | 24 | private void OnDrawGizmos() 25 | { 26 | Gizmos.color = new Color(0.0f, 0.0f, 0.6f, 0.5f); 27 | Gizmos.matrix = transform.localToWorldMatrix; 28 | Gizmos.DrawSphere(Vector3.zero, 0.1f); 29 | } 30 | 31 | /// 32 | /// Spawn the object this marker describes. 33 | /// You can call this from a trigger! 34 | /// 35 | public void Spawn() 36 | { 37 | Debug.Log("Loading Anvil Asset: " + ResourceDefs.AnvilAssetResources[prefab]); 38 | FVRObject obj = Resources.Load(ResourceDefs.AnvilAssetResources[prefab]); 39 | GameObject go = Instantiate(obj.GetGameObject(), transform.position, transform.rotation, ObjectReferences.CustomScene.transform); 40 | go.SetActive(true); 41 | } 42 | 43 | } 44 | } -------------------------------------------------------------------------------- /WurstMod/MappingComponents/Generic/CustomScene.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | using WurstMod.Runtime; 4 | using WurstMod.Shared; 5 | 6 | #if UNITY_EDITOR 7 | using WurstMod.UnityEditor; 8 | #else 9 | using Valve.VR.InteractionSystem; 10 | using FistVR; 11 | #endif 12 | 13 | namespace WurstMod.MappingComponents.Generic 14 | { 15 | public class CustomScene : ComponentProxy 16 | { 17 | [HideInInspector] public string SceneName; 18 | [HideInInspector] public string Author; 19 | [HideInInspector] public string Gamemode; 20 | [HideInInspector] public string Description; 21 | [HideInInspector] public Material Skybox; 22 | 23 | [HideInInspector] public StringKeyValue[] ExtraData; 24 | 25 | [Header("Scene Settings")] public float MaxProjectileRange = 500f; 26 | [Header("AI Settings")] public int NumEntitiesToCheckPerFrame = 1; 27 | 28 | public int PlayerIFF = 0; 29 | 30 | #if UNITY_EDITOR 31 | public override void OnExport(ExportErrors err) 32 | { 33 | Skybox = RenderSettings.skybox; 34 | } 35 | #endif 36 | 37 | public void TeleportPlayer(Transform to) 38 | { 39 | #if !UNITY_EDITOR 40 | // Proxy to the game's teleport method 41 | GM.CurrentMovementManager.TeleportToPoint(to.position, true, to.position + transform.forward); 42 | #endif 43 | } 44 | public void KillPlayer() 45 | { 46 | #if !UNITY_EDITOR 47 | // Damage the player directly, but then play the death noise. 48 | // This is required because otherwise the invincible power-up blocks the damage. 49 | GM.CurrentPlayerBody.RegisterPlayerHit(GM.CurrentPlayerBody.Health + 5, true); 50 | FVRPlayerHitbox hb = GM.CurrentPlayerBody.Hitboxes[0]; 51 | hb.m_aud.PlayOneShot(hb.AudClip_Reset); 52 | #endif 53 | } 54 | 55 | public override void InitializeComponent() 56 | { 57 | #if !UNITY_EDITOR 58 | // This component is responsible for resolving many of the global/builtin things about a level. 59 | // Skybox 60 | if (Skybox != null) 61 | { 62 | RenderSettings.skybox = Skybox; 63 | RenderSettings.skybox.RefreshShader(); 64 | DynamicGI.UpdateEnvironment(); 65 | } 66 | 67 | // Shaders 68 | foreach (MeshRenderer ii in GetComponentsInChildren(true)) 69 | { 70 | foreach (Material jj in ii.materials) 71 | { 72 | jj.RefreshShader(); 73 | } 74 | } 75 | 76 | // Particle Shaders 77 | foreach (ParticleSystemRenderer ii in GetComponentsInChildren(true)) 78 | { 79 | ii.materials.ForEach(x => x.RefreshShader()); 80 | } 81 | 82 | // Terrain 83 | foreach (Terrain ii in GetComponentsInChildren(true)) 84 | { 85 | ii.materialTemplate.RefreshShader(); 86 | ii.terrainData.treePrototypes.ForEach(x => x.prefab.layer = LayerMask.NameToLayer("Environment")); 87 | foreach (TreePrototype jj in ii.terrainData.treePrototypes) 88 | { 89 | jj.prefab.layer = LayerMask.NameToLayer("Environment"); 90 | MeshRenderer[] mrs = jj.prefab.GetComponentsInChildren(); 91 | mrs.ForEach(x => x.material.RefreshShader()); 92 | } 93 | 94 | foreach (TreeInstance jj in ii.terrainData.treeInstances) 95 | { 96 | GameObject copiedTree = Instantiate(ii.terrainData.treePrototypes[jj.prototypeIndex].prefab, 97 | ii.transform); 98 | copiedTree.transform.localPosition = new Vector3(ii.terrainData.size.x * jj.position.x, 99 | ii.terrainData.size.y * jj.position.y, ii.terrainData.size.z * jj.position.z); 100 | copiedTree.transform.localScale = new Vector3(jj.widthScale, jj.heightScale, jj.widthScale); 101 | copiedTree.transform.localEulerAngles = new Vector3(0f, jj.rotation, 0f); 102 | } 103 | 104 | ii.terrainData.treeInstances = new TreeInstance[0]; 105 | } 106 | 107 | // Set the max range on the scene settings 108 | // TODO: There are probably a lot of settings we should carry over here. 109 | ObjectReferences.FVRSceneSettings.MaxProjectileRange = MaxProjectileRange; 110 | ObjectReferences.FVRSceneSettings.DefaultPlayerIFF = PlayerIFF; 111 | GM.CurrentPlayerBody.SetPlayerIFF(PlayerIFF); 112 | #endif 113 | } 114 | 115 | [Serializable] 116 | public struct StringKeyValue 117 | { 118 | public string Key; 119 | public string Value; 120 | } 121 | } 122 | } -------------------------------------------------------------------------------- /WurstMod/MappingComponents/Generic/FVRHandGrabPoint.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace WurstMod.MappingComponents.Generic 4 | { 5 | /// 6 | /// Place this class on any geometry you want to be able to climb, like ladders! 7 | /// Climbable geometry must be in the Interactable layer. 8 | /// 9 | public class FVRHandGrabPoint : ComponentProxy 10 | { 11 | [Tooltip("A DISABLED gameObject visualizing the ability to grab this object. This is the glowy bit when your hands are near a ladder.")] 12 | public GameObject UXGeo_Hover; 13 | 14 | [Tooltip("A DISABLED gameObject visualizing you grabbing this object. This is the glowy bit when you are grabbing a ladder.")] 15 | public GameObject UXGeo_Held; 16 | 17 | public override void InitializeComponent() 18 | { 19 | FistVR.FVRHandGrabPoint real = gameObject.AddComponent(); 20 | 21 | real.UXGeo_Hover = UXGeo_Hover; 22 | real.UXGeo_Held = UXGeo_Held; 23 | real.PositionInterpSpeed = 1; 24 | real.RotationInterpSpeed = 1; 25 | 26 | // Messy math for interaction distance. 27 | Collider proxyCol = GetComponent(); 28 | Vector3 extents = proxyCol.bounds.extents; 29 | real.EndInteractionDistance = 2.5f * Mathf.Abs(Mathf.Max(extents.x, extents.y, extents.z)); 30 | 31 | Destroy(this); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /WurstMod/MappingComponents/Generic/FVRReverbEnvironment.cs: -------------------------------------------------------------------------------- 1 | using WurstMod.Runtime; 2 | 3 | namespace WurstMod.MappingComponents.Generic 4 | { 5 | /// 6 | /// Placed on an object with a non-trigger box collider, on the NoCol layer. 7 | /// 8 | public class FVRReverbEnvironment : ComponentProxy 9 | { 10 | public enum FVRSoundEnvironment 11 | { 12 | None = 0, 13 | Forest = 1, 14 | InsideNarrow = 10, 15 | InsideSmall = 11, 16 | InsideWarehouse = 12, 17 | InsideNarrowSmall = 13, 18 | InsideLarge = 14, 19 | InsideWarehouseSmall = 15, 20 | InsideMedium = 16, 21 | InsideLargeHighCeiling = 17, 22 | OutsideOpen = 20, 23 | OutsideEnclosed = 21, 24 | OutsideEnclosedNarrow = 22, 25 | SniperRange = 30, 26 | ShootingRange = 31, 27 | } 28 | 29 | public FVRSoundEnvironment Environment; 30 | public int Priority; 31 | 32 | public override void InitializeComponent() 33 | { 34 | FistVR.FVRReverbEnvironment real = gameObject.AddComponent(); 35 | 36 | real.Environment = (FistVR.FVRSoundEnvironment) Environment; 37 | real.Priority = Priority; 38 | 39 | ObjectReferences.ReverbSystem.Environments.Add(real); 40 | 41 | Destroy(this); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /WurstMod/MappingComponents/Generic/HandTrigger.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEngine.Events; 3 | #if UNITY_EDITOR 4 | using WurstMod.UnityEditor; 5 | #endif 6 | 7 | namespace WurstMod.MappingComponents.Generic 8 | { 9 | [RequireComponent(typeof(Collider))] 10 | public class HandTrigger : ComponentProxy 11 | { 12 | public UnityEvent Triggered; 13 | 14 | public void OnTriggerEnter(Collider other) 15 | { 16 | Triggered.Invoke(); 17 | } 18 | 19 | #if UNITY_EDITOR 20 | public override void OnExport(ExportErrors err) 21 | { 22 | // This needs to be a trigger on the interactable layer 23 | GetComponent().isTrigger = true; 24 | gameObject.layer = LayerMask.NameToLayer("Interactable"); 25 | } 26 | #endif 27 | } 28 | } -------------------------------------------------------------------------------- /WurstMod/MappingComponents/Generic/ItemSpawner.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using FistVR; 3 | using UnityEngine; 4 | using WurstMod.Runtime; 5 | 6 | namespace WurstMod.MappingComponents.Generic 7 | { 8 | public class ItemSpawner : ComponentProxy 9 | { 10 | public string ObjectId; 11 | public bool SpawnOnLoad; 12 | 13 | public override void InitializeComponent() 14 | { 15 | if (SpawnOnLoad) Spawn(); 16 | } 17 | 18 | private void OnDrawGizmos() 19 | { 20 | Gizmos.color = new Color(0.0f, 0.0f, 0.6f, 0.5f); 21 | Gizmos.matrix = transform.localToWorldMatrix; 22 | Gizmos.DrawSphere(Vector3.zero, 0.1f); 23 | } 24 | 25 | /// 26 | /// Spawn the object this marker describes. 27 | /// You can call this from a trigger! 28 | /// 29 | public void Spawn() 30 | { 31 | StartCoroutine(SpawnAsync()); 32 | } 33 | 34 | private IEnumerator SpawnAsync() 35 | { 36 | #if !UNITY_EDITOR 37 | FVRObject obj = IM.OD[ObjectId]; 38 | var callback = obj.GetGameObjectAsync(); 39 | yield return callback; 40 | GameObject go = Instantiate(callback.Result, transform.position, transform.rotation, 41 | ObjectReferences.CustomScene.transform); 42 | go.SetActive(true); 43 | #else 44 | yield return null; 45 | #endif 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /WurstMod/MappingComponents/Generic/PMat.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using WurstMod.Shared; 3 | 4 | namespace WurstMod.MappingComponents.Generic 5 | { 6 | public class PMat : ComponentProxy 7 | { 8 | [Tooltip("Not all static colliders (anything a bullet might hit) have a def. Is this for penetrative properties? Legacy?")] 9 | public ResourceDefs.PMat def; 10 | [Tooltip("It seems like all static colliders (anything a bullet might hit) have a matDef.")] 11 | public ResourceDefs.MatDef matDef; 12 | 13 | 14 | public override void InitializeComponent() 15 | { 16 | FistVR.PMat real = gameObject.AddComponent(); 17 | 18 | real.Def = ResourceDefs.PMatResources.ContainsKey(def) ? Resources.Load(ResourceDefs.PMatResources[def]) : null; 19 | real.MatDef = ResourceDefs.MatDefResources.ContainsKey(matDef) ? Resources.Load(ResourceDefs.MatDefResources[matDef]) : null; 20 | 21 | Destroy(this); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /WurstMod/MappingComponents/Generic/PlayerTrigger.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEngine.Events; 3 | #if UNITY_EDITOR 4 | using WurstMod.UnityEditor; 5 | #endif 6 | namespace WurstMod.MappingComponents.Generic 7 | { 8 | [RequireComponent(typeof(Collider))] 9 | public class PlayerTrigger : ComponentProxy 10 | { 11 | public UnityEvent Enter; 12 | public UnityEvent Exit; 13 | 14 | private int _inTrigger; 15 | 16 | private void OnTriggerEnter(Collider other) 17 | { 18 | if (_inTrigger != 0) return; 19 | Enter.Invoke(); 20 | _inTrigger++; 21 | } 22 | 23 | private void OnTriggerExit(Collider other) 24 | { 25 | _inTrigger--; 26 | if (_inTrigger != 0) return; 27 | Exit.Invoke(); 28 | } 29 | 30 | #if UNITY_EDITOR 31 | public override void OnExport(ExportErrors err) 32 | { 33 | // We needs this to be a trigger on the ColOnlyHead layer. 34 | GetComponent().isTrigger = true; 35 | gameObject.layer = LayerMask.NameToLayer("ColOnlyHead"); 36 | } 37 | #endif 38 | } 39 | } -------------------------------------------------------------------------------- /WurstMod/MappingComponents/Generic/SosigSpawner.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Linq; 3 | using FistVR; 4 | using UnityEngine; 5 | using WurstMod.Runtime; 6 | using WurstMod.Shared; 7 | #if UNITY_EDITOR 8 | using WurstMod.UnityEditor; 9 | #endif 10 | using Random = UnityEngine.Random; 11 | 12 | namespace WurstMod.MappingComponents.Generic 13 | { 14 | public class SosigSpawner : ComponentProxy 15 | { 16 | [Tooltip("A list of Sosig types to spawn from")] 17 | public Enums.P_SosigEnemyID[] SosigTypes; 18 | 19 | [Tooltip("The delay between this spawner becoming active and when it starts spawning")] 20 | public float SpawnDelay = 10f; 21 | 22 | [Tooltip("The delay between the spawning of each Sosig")] 23 | public float SpawnInterval = 5f; 24 | 25 | [Tooltip("The number of Sosigs to spawn before automatically deactivating (0 for infinite)")] 26 | public int SpawnCount = 5; 27 | 28 | [Tooltip("Whether or not this spawner is active by default")] 29 | public bool Active = true; 30 | 31 | [Header("Spawn Options")] [Tooltip("Whether to spawn the Sosig activated or not")] 32 | public bool SpawnActivated; 33 | 34 | [Tooltip("Sets the Sosig's IFF (Team). Values 5 and above get randomized")] 35 | public int IFF; 36 | 37 | [Tooltip("Spawns the Sosig with full ammo")] 38 | public bool SpawnWithFullAmmo; 39 | 40 | [Tooltip("Not sure what this does. Recommended to just leave at 0")] 41 | public int EquipmentMode; 42 | 43 | [Tooltip("The state to spawn the Sosig in. 0 = Disabled, 1 = Guard, 2 = Wander, 3 = Assault")] 44 | public int SpawnState; 45 | 46 | [Tooltip("The position of the Sosig's attack / guard position")] 47 | public Vector3 SosigTargetPosition; 48 | 49 | public Vector3 SosigTargetRotation; 50 | 51 | [Tooltip("Set this a transform to make the Sosigs spawn with it's position and rotation as it's target.")] 52 | public Transform SosigTransformTarget; 53 | 54 | // This needs to be a ScriptableObject because otherwise Unity throws a fit 55 | private ScriptableObject[] _enemyTemplates; 56 | private IEnumerator _coroutine; 57 | 58 | private void Awake() 59 | { 60 | // Convert the sosig types to the templates 61 | _enemyTemplates = SosigTypes.Select(x => 62 | ManagerSingleton.Instance.odicSosigObjsByID[(SosigEnemyID) x] as ScriptableObject).ToArray(); 63 | } 64 | 65 | private void Start() 66 | { 67 | // If we want to start active, do that. 68 | SetActive(Active); 69 | } 70 | 71 | /// 72 | /// Sets this spawner active or inactive. Setting an active spawner to inactive will instantly 73 | /// cancel any further spawning and setting an active spawner to active will reset the spawner state. 74 | /// 75 | /// 76 | public void SetActive(bool active) 77 | { 78 | // Update the variable 79 | Active = active; 80 | 81 | // We stop the coroutine either way 82 | if (_coroutine != null) StopCoroutine(_coroutine); 83 | 84 | // But we only restart it if we're activating 85 | if (!Active) return; 86 | 87 | // Reset the coroutine 88 | _coroutine = SpawnCoroutine(); 89 | StartCoroutine(_coroutine); 90 | } 91 | 92 | /// 93 | /// Spawns a single Sosig 94 | /// 95 | public void Spawn() 96 | { 97 | #if !UNITY_EDITOR 98 | // Pick a random template and spawn the Sosig 99 | var template = _enemyTemplates[Random.Range(0, _enemyTemplates.Length)] as SosigEnemyTemplate; 100 | SosigSpawnerHelper.SpawnSosigWithTemplate(template, new SosigSpawnerHelper.SpawnOptions 101 | { 102 | SpawnActivated = SpawnActivated, 103 | IFF = IFF, 104 | SpawnWithFullAmmo = SpawnWithFullAmmo, 105 | EquipmentMode = EquipmentMode, 106 | SpawnState = SpawnState, 107 | SosigTargetPosition = SosigTransformTarget ? SosigTransformTarget.position : SosigTargetPosition, 108 | SosigTargetRotation = SosigTransformTarget ? SosigTransformTarget.rotation.eulerAngles : SosigTargetRotation 109 | }, transform.position, transform.forward); 110 | #endif 111 | } 112 | 113 | /// 114 | /// Coroutine to handle spawning logic 115 | /// 116 | private IEnumerator SpawnCoroutine() 117 | { 118 | // Wait for the activation delay 119 | yield return new WaitForSeconds(SpawnDelay); 120 | 121 | // Repeat as many times as needed 122 | var counter = 0u; 123 | while (counter < SpawnCount || SpawnCount == 0) 124 | { 125 | // Spawn a sosig 126 | Spawn(); 127 | 128 | // Then wait for the interval delay to expire 129 | yield return new WaitForSeconds(SpawnInterval); 130 | 131 | // Increment the counter. This *might* break if it's set to infinite spawning and we exceed the 132 | // max 32-bit uint value but I don't think we need to worry about that. 133 | counter++; 134 | } 135 | } 136 | 137 | #if UNITY_EDITOR 138 | public override void OnExport(ExportErrors err) 139 | { 140 | if (SosigTypes.Length == 0) err.AddError("Sosig Spawner has no types to spawn!", this); 141 | if (SpawnDelay < 0) err.AddError("Sosig Spawner cannot have a spawn delay of less than zero", this); 142 | if (SpawnInterval < 0) err.AddError("Sosig Spawner cannot have a spawn interval of less than zero", this); 143 | if (SpawnCount < 0) err.AddError("Sosig Spawner cannot have a spawn count of less than zero", this); 144 | } 145 | #endif 146 | 147 | private void OnDrawGizmos() 148 | { 149 | Extensions.GenericGizmoSphere(new Color(0.8f, 0f, 0f, 0.5f), Vector3.zero, 0.25f, transform); 150 | } 151 | } 152 | } -------------------------------------------------------------------------------- /WurstMod/MappingComponents/Generic/Target.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using UnityEngine; 3 | using UnityEngine.Events; 4 | 5 | namespace WurstMod.MappingComponents.Generic 6 | { 7 | /// 8 | /// A target like the ones in the friendly 45 range. 9 | /// You can assign a sound that plays on hit and define a range for random volume, pitch, and play speed. 10 | /// Additionally, you can include a Unity event for some advanced behaviour. 11 | /// 12 | public class Target : ComponentProxy 13 | { 14 | [Tooltip("Clip to be played when shot.")] 15 | public List clips; 16 | 17 | [Tooltip("Range of volume for clip, chosen from randomly each time the target is shot.")] 18 | public Vector2 volumeRange = new Vector2(0.9f, 1.1f); 19 | 20 | [Tooltip("Range of pitch for clip, chosen from randomly each time the target is shot.")] 21 | public Vector2 pitchRange = new Vector2(0.97f, 1.03f); 22 | 23 | [Tooltip("Range of speed for clip, chosen from randomly each time the target is shot.")] 24 | public Vector2 speedRange = new Vector2(1f, 1f); 25 | 26 | [Tooltip("A Unity event, useful for triggering events that exist in base Unity code.")] 27 | public UnityEvent shotEvent; 28 | 29 | public override void InitializeComponent() 30 | { 31 | FistVR.ReactiveSteelTarget baseTarget = gameObject.AddComponent(); 32 | baseTarget.HitEvent = new FistVR.AudioEvent(); 33 | baseTarget.HitEvent.Clips = clips; 34 | baseTarget.HitEvent.VolumeRange = volumeRange; 35 | baseTarget.HitEvent.PitchRange = pitchRange; 36 | baseTarget.HitEvent.ClipLengthRange = speedRange; 37 | 38 | if (baseTarget.HitEvent.Clips.Count == 0) 39 | { 40 | baseTarget.HitEvent.Clips = new List(); 41 | baseTarget.HitEvent.Clips.Add(new AudioClip()); 42 | } 43 | 44 | baseTarget.BulletHolePrefabs = new GameObject[0]; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /WurstMod/MappingComponents/Generic/Trigger.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using UnityEngine; 5 | using UnityEngine.Events; 6 | using WurstMod.Shared; 7 | 8 | namespace WurstMod.MappingComponents.Generic 9 | { 10 | public enum TriggeredBy { Player, Sosig, Collider } 11 | public enum TriggerType { Any, Each } 12 | public enum TriggerShape { Cube, Sphere } 13 | 14 | public class Trigger : MonoBehaviour 15 | { 16 | // These triggers are going to be fairly expensive because we cannot use base Unity triggers. 17 | // I think this is due to some wonky raycast logic used by Anton, probably not fixable. 18 | 19 | // Inspectables 20 | [Header("What to Detect")] 21 | [Tooltip("What this is triggered by.")] 22 | public TriggeredBy triggeredBy = TriggeredBy.Collider; 23 | [Tooltip("Collider mode only: If there are elements in the whitelist, only objects with those names will be detected.")] 24 | public List whitelist = new List(); 25 | [Tooltip("Collider mode only: Objects with names in the blacklist will be ignored.")] 26 | public List blacklist = new List(); 27 | 28 | [Header("How to Detect")] 29 | [Tooltip("Any: Trigger Enter when number of valid objects in trigger goes from 0 to 1, and trigger Exit on 1 to 0\nEach: Trigger Enter when number of valid objects in trigger increases, and trigger Exit when it decreases.")] 30 | [HideInInspector] // TODO: Each has some duplicated object bugs that are going to be very amusing to fix. For now, I will ignore them. Any mode is plenty useful as is. 31 | public TriggerType triggerType = TriggerType.Any; 32 | 33 | [Header("Where to Detect")] 34 | [Tooltip("The shape of the trigger.")] 35 | public TriggerShape triggerShape = TriggerShape.Cube; 36 | [Tooltip("When in cube mode, this controls the size of the cube.")] 37 | public Vector3 cubeShape = Vector3.one; 38 | [Tooltip("When in sphere mode, this controls the size of the sphere.")] 39 | public float sphereRadius = 1f; 40 | 41 | [Header("What to Do")] 42 | [Tooltip("Actions to run when trigger is entered.")] 43 | public UnityEvent enterEvent; 44 | [Tooltip("Actions to run when trigger is exited.")] 45 | public UnityEvent exitEvent; 46 | 47 | // Internal for Controller 48 | private static Trigger controller; 49 | private static List allTriggers = new List(); 50 | private static readonly int checkPerFrame = 10; // How many triggers we check each frame. 51 | private static bool checking = false; // Flag to control coroutine restarting. 52 | 53 | // Instance 54 | private HashSet colliders = new HashSet(); 55 | 56 | private void OnDrawGizmos() 57 | { 58 | 59 | switch (triggerShape) 60 | { 61 | case TriggerShape.Cube: 62 | Extensions.GenericGizmoCubeOutline(Color.green, Vector3.zero, cubeShape, transform); 63 | break; 64 | case TriggerShape.Sphere: 65 | Extensions.GenericGizmoSphereOutline(Color.green, Vector3.zero, sphereRadius, transform); 66 | break; 67 | } 68 | } 69 | 70 | private void Awake() 71 | { 72 | // Set the singleton controller to the first Trigger to wake up. 73 | if (controller == null) controller = this; 74 | 75 | // Add ourself to the list of triggers. 76 | allTriggers.Add(this); 77 | } 78 | 79 | private void OnDestroy() 80 | { 81 | // Reload-safe logic. 82 | allTriggers.Remove(this); 83 | if (allTriggers.Count == 0) 84 | { 85 | controller = null; 86 | } 87 | } 88 | 89 | private void Update() 90 | { 91 | // The update logic is handled by the controller trigger only. 92 | if (controller == this) 93 | { 94 | if (!checking) 95 | { 96 | StartCoroutine(CheckAllTriggers()); 97 | } 98 | } 99 | } 100 | 101 | private static IEnumerator CheckAllTriggers() 102 | { 103 | checking = true; 104 | for (int ii = 0; ii < allTriggers.Count; ii++) 105 | { 106 | allTriggers[ii].CheckTrigger(); 107 | 108 | // Skip a frame every {checkPerFrame} triggers. 109 | if (ii > 0 && ii % checkPerFrame == 0) yield return null; 110 | } 111 | checking = false; 112 | } 113 | 114 | private void CheckTrigger() 115 | { 116 | Collider[] collidersInOverlap = triggerShape == TriggerShape.Cube ? 117 | Physics.OverlapBox(transform.position, cubeShape) : 118 | Physics.OverlapSphere(transform.position, sphereRadius); 119 | 120 | // Check enter 121 | foreach (Collider col in collidersInOverlap) 122 | { 123 | if (!colliders.Contains(col) && IsValidForTrigger(col)) 124 | { 125 | colliders.Add(col); 126 | Debug.Log("ENTER: " + col.gameObject.name); 127 | 128 | if (colliders.Count == 1 || triggerType == TriggerType.Each) enterEvent.Invoke(); 129 | } 130 | } 131 | 132 | // Check exit 133 | Collider[] collidersInHashset = colliders.ToArray(); 134 | foreach (Collider col in collidersInHashset) 135 | { 136 | if (!collidersInOverlap.Contains(col)) 137 | { 138 | colliders.Remove(col); 139 | Debug.Log("EXIT: " + col.gameObject.name); 140 | 141 | if (colliders.Count == 0 || triggerType == TriggerType.Each) exitEvent.Invoke(); 142 | } 143 | } 144 | } 145 | 146 | private bool IsValidForTrigger(Collider col) 147 | { 148 | if (triggeredBy == TriggeredBy.Player && col.gameObject.name == "Hitbox_Head") return true; 149 | if (triggeredBy == TriggeredBy.Sosig && col.gameObject.name == "AIEntity") return true; 150 | if (triggeredBy == TriggeredBy.Collider) 151 | { 152 | if (whitelist.Count > 0 && !whitelist.Contains(col.gameObject.name)) return false; 153 | if (blacklist.Count > 0 && blacklist.Contains(col.gameObject.name)) return false; 154 | return true; 155 | } 156 | return false; 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /WurstMod/MappingComponents/Sandbox/GenericPrefab.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | using WurstMod.Runtime; 4 | using WurstMod.Shared; 5 | 6 | namespace WurstMod.MappingComponents.Sandbox 7 | { 8 | public enum Prefab 9 | { 10 | ItemSpawner, 11 | Destructobin, 12 | SosigSpawner, 13 | WhizzBangADinger, 14 | WhizzBangADingerDetonator 15 | } 16 | 17 | public class GenericPrefab : ComponentProxy 18 | { 19 | /// 20 | /// Select what kind of object this is, the ghost will update to match. 21 | /// 22 | public Prefab objectType; 23 | 24 | void OnDrawGizmos() 25 | { 26 | switch (objectType) 27 | { 28 | case Prefab.ItemSpawner: 29 | Extensions.GenericGizmoCube(new Color(0.4f, 0.4f, 0.9f, 0.5f), new Vector3(0f, 0.7f, 0.25f), 30 | new Vector3(2.3f, 1.2f, 0.5f), Vector3.forward, transform); 31 | break; 32 | case Prefab.Destructobin: 33 | Extensions.GenericGizmoCube(new Color(0.4f, 0.4f, 0.9f, 0.5f), new Vector3(0f, 0.525f, 0f), 34 | new Vector3(0.65f, 1.05f, 0.65f), Vector3.left, transform); 35 | break; 36 | case Prefab.SosigSpawner: 37 | Extensions.GenericGizmoCube(new Color(0.9f, 0.4f, 0.4f, 0.5f), new Vector3(0f, 0.11f, 0f), 38 | new Vector3(0.3f, 0.43f, 0.05f), Vector3.back, transform); 39 | break; 40 | case Prefab.WhizzBangADinger: 41 | Extensions.GenericGizmoCube(new Color(0.9f, 0.9f, 0.4f, 0.5f), new Vector3(0f, 0.425f, -0.05f), 42 | new Vector3(0.4f, 0.85f, 0.5f), Vector3.forward, transform); 43 | break; 44 | case Prefab.WhizzBangADingerDetonator: 45 | Extensions.GenericGizmoCube(new Color(0.9f, 0.9f, 0.4f, 0.5f), new Vector3(0f, 0f, 0.1f), 46 | new Vector3(0.05f, 0.05f, 0.33f), Vector3.forward, transform); 47 | break; 48 | } 49 | } 50 | 51 | public override void InitializeComponent() 52 | { 53 | GameObject original; 54 | switch (objectType) 55 | { 56 | case Prefab.ItemSpawner: 57 | original = ObjectReferences.ItemSpawnerDonor; 58 | break; 59 | case Prefab.Destructobin: 60 | original = ObjectReferences.DestructobinDonor; 61 | break; 62 | case Prefab.SosigSpawner: 63 | original = ObjectReferences.SosigSpawnerDonor; 64 | break; 65 | case Prefab.WhizzBangADinger: 66 | original = ObjectReferences.WhizzBangADingerDonor; 67 | break; 68 | case Prefab.WhizzBangADingerDetonator: 69 | original = ObjectReferences.BangerDetonatorDonor; 70 | break; 71 | default: 72 | throw new ArgumentOutOfRangeException(); 73 | } 74 | 75 | GameObject copy = Instantiate(original); 76 | copy.transform.SetPositionAndRotation(transform.position, transform.rotation); 77 | copy.SetActive(true); 78 | Destroy(this); 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /WurstMod/MappingComponents/Sandbox/GroundPanel.cs: -------------------------------------------------------------------------------- 1 | using FistVR; 2 | using UnityEngine; 3 | using WurstMod.Runtime; 4 | 5 | namespace WurstMod.MappingComponents.Sandbox 6 | { 7 | public class GroundPanel : ComponentProxy 8 | { 9 | // For some reason, the SosigTestingPanels aren't centered at the base. 10 | private static readonly Vector3 PositionOffset = new Vector3(0f, 0.768f, 0f); 11 | private static readonly Vector3 RotationOffset = new Vector3(-60f, 0f, 0f); 12 | 13 | public Canvas Canvas; 14 | 15 | public override void InitializeComponent() 16 | { 17 | // Make a copy of the SosigTestingPanel 18 | var panel = Instantiate(ObjectReferences.GroundPanel, transform); 19 | 20 | // Correct it's position and rotation 21 | var correctedPos = transform.position + PositionOffset; 22 | var correctedRot = Quaternion.Euler(transform.rotation.eulerAngles + RotationOffset); 23 | panel.transform.SetPositionAndRotation(correctedPos, correctedRot); 24 | panel.transform.localScale = Vector3.one; 25 | 26 | // Remove the existing canvas and components 27 | var originalCanvas = panel.GetComponentInChildren(); 28 | var originalPos = originalCanvas.transform.position; 29 | var originalRot = originalCanvas.transform.rotation; 30 | Destroy(panel.GetComponent()); 31 | Destroy(originalCanvas.gameObject); 32 | 33 | // Replace it with the new canvas 34 | var parent = panel.transform.Find("Selections"); 35 | Canvas.transform.parent = parent; 36 | Canvas.transform.SetPositionAndRotation(originalPos, originalRot); 37 | } 38 | 39 | public void OnDrawGizmos() 40 | { 41 | // TODO: Find out what shape to draw this 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /WurstMod/MappingComponents/Sandbox/PointableButton.cs: -------------------------------------------------------------------------------- 1 | using FistVR; 2 | using UnityEngine; 3 | using UnityEngine.UI; 4 | using WurstMod.Runtime; 5 | 6 | #if UNITY_EDITOR 7 | using WurstMod.UnityEditor; 8 | #endif 9 | 10 | namespace WurstMod.MappingComponents.Sandbox 11 | { 12 | [RequireComponent(typeof(BoxCollider))] 13 | public class PointableButton : ComponentProxy 14 | { 15 | // These are the colors for almost every button in the game 16 | public Color ColorUnselected = new Color32(0x29, 0x6E, 0xEA, 0xFF); 17 | public Color ColorSelected = new Color32(0xB1, 0xC9, 0xF4, 0xFF); 18 | 19 | [Tooltip("I think this is used for setting the color of a material? Not sure. Leave as-is.")] 20 | public string ColorName = "_Color"; 21 | 22 | 23 | [Header("Component References")] 24 | public Button Button; 25 | 26 | public Image Image; 27 | public Text Text; 28 | public Renderer Rend; 29 | 30 | public override void InitializeComponent() 31 | { 32 | var proxied = gameObject.AddComponent(); 33 | proxied.ColorUnselected = ColorUnselected; 34 | proxied.ColorSelected = ColorSelected; 35 | proxied.Button = Button; 36 | proxied.Image = Image; 37 | proxied.Text = Text; 38 | proxied.Rend = Rend; 39 | proxied.ColorName = ColorName; 40 | 41 | // Borrow the sprite 42 | proxied.Image.sprite = ObjectReferences.ButtonDonor.Image.sprite; 43 | 44 | // Force an update or something. Idk. 45 | proxied.ForceUpdate(); 46 | 47 | Destroy(this); 48 | } 49 | 50 | #if UNITY_EDITOR 51 | public override void OnExport(ExportErrors err) 52 | { 53 | // Set the collider's size 54 | var collider = GetComponent(); 55 | var size = GetComponent().sizeDelta; 56 | collider.size = new Vector3(size.x, size.y, 1); 57 | } 58 | #endif 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /WurstMod/MappingComponents/Sandbox/Spawn.cs: -------------------------------------------------------------------------------- 1 | using FistVR; 2 | using UnityEngine; 3 | using WurstMod.Runtime; 4 | using WurstMod.Shared; 5 | 6 | namespace WurstMod.MappingComponents.Sandbox 7 | { 8 | public class Spawn : ComponentProxy 9 | { 10 | void OnDrawGizmos() 11 | { 12 | Extensions.GenericGizmoSphere(new Color(0.0f, 0.8f, 0.8f, 0.5f), Vector3.zero, 0.25f, transform); 13 | } 14 | 15 | public override void InitializeComponent() 16 | { 17 | ObjectReferences.CameraRig.transform.position = transform.position; 18 | if (ManagerSingleton.Instance != null && GM.CurrentSceneSettings != null) 19 | GM.CurrentSceneSettings.DeathResetPoint = transform; 20 | Destroy(this); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /WurstMod/MappingComponents/TakeAndHold/AttackVector.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using UnityEngine; 3 | using WurstMod.Shared; 4 | 5 | namespace WurstMod.MappingComponents.TakeAndHold 6 | { 7 | public class AttackVector : MonoBehaviour 8 | { 9 | [Tooltip("During Holds, when this AttackVector is chosen, enemy sosigs will spawn at these points. Use 3 or more points to be safe.")] 10 | public List SpawnPoints_Sosigs_Attack; 11 | 12 | [Tooltip("Usually placed at this AttackVector's entrance to the hold point. Like, where the red barrier appears.")] 13 | public Transform GrenadeVector; 14 | 15 | [Tooltip("Usually 30.")] 16 | public float GrenadeRandAngle; 17 | 18 | [Tooltip("Usually {3,8}.")] 19 | public Vector2 GrenadeVelRange; 20 | 21 | private void OnDrawGizmos() 22 | { 23 | if (SpawnPoints_Sosigs_Attack != null && SpawnPoints_Sosigs_Attack.Count != 0) Extensions.GenericGizmoSphere(new Color(0.8f, 0f, 0f, 0.5f), Vector3.zero, 0.25f, SpawnPoints_Sosigs_Attack.ToArray()); 24 | if (GrenadeVector != null) Extensions.GenericGizmoCube(new Color(0.1f, 0.5f, 0.1f, 0.5f), Vector3.zero, 0.5f * Vector3.one, Vector3.forward, GrenadeVector); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /WurstMod/MappingComponents/TakeAndHold/ForcedSpawn.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace WurstMod.MappingComponents.TakeAndHold 4 | { 5 | [RequireComponent(typeof(TNH_SupplyPoint))] 6 | public class ForcedSpawn : MonoBehaviour 7 | { 8 | [Tooltip("True if this supply point ONLY be used for spawn, or false if it can spawn again as a supply point later.")] 9 | public bool spawnOnly = false; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /WurstMod/MappingComponents/TakeAndHold/Respawn.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using WurstMod.Runtime; 3 | using WurstMod.Shared; 4 | 5 | namespace WurstMod.MappingComponents.TakeAndHold 6 | { 7 | class Respawn : ComponentProxy 8 | { 9 | private void OnDrawGizmos() 10 | { 11 | Extensions.GenericGizmoSphere(new Color(0.0f, 0.8f, 0.8f, 0.5f), Vector3.zero, 0.25f, transform); 12 | } 13 | 14 | public override void InitializeComponent() 15 | { 16 | ObjectReferences.ResetPoint.transform.position = transform.position; 17 | 18 | Destroy(this); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /WurstMod/MappingComponents/TakeAndHold/Scoreboard.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using WurstMod.Runtime; 3 | using WurstMod.Shared; 4 | 5 | namespace WurstMod.MappingComponents.TakeAndHold 6 | { 7 | class Scoreboard : ComponentProxy 8 | { 9 | private void OnDrawGizmos() 10 | { 11 | Extensions.GenericGizmoCube(new Color(0.4f, 0.4f, 0.9f, 0.5f), new Vector3(0f, 0f, 0f), new Vector3(2f, 2f, 0.1f), Vector3.back, transform); 12 | transform.Rotate(0, -45, 0); 13 | Extensions.GenericGizmoCube(new Color(0.4f, 0.4f, 0.9f, 0.5f), new Vector3(-1.771f, 0f, 0.743f), new Vector3(2f, 2f, 0.1f), Vector3.back, transform); 14 | transform.Rotate(0, 45, 0); 15 | } 16 | 17 | public override void InitializeComponent() 18 | { 19 | ObjectReferences.ManagerDonor.ScoreDisplayPoint = transform; 20 | } 21 | 22 | 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /WurstMod/MappingComponents/TakeAndHold/ScoreboardArea.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | using WurstMod.Runtime; 4 | using WurstMod.Shared; 5 | 6 | namespace WurstMod.MappingComponents.TakeAndHold 7 | { 8 | [Obsolete] 9 | [AddComponentMenu("")] 10 | public class ScoreboardArea : ComponentProxy 11 | { 12 | private void OnDrawGizmos() 13 | { 14 | Extensions.GenericGizmoCube(new Color(0.4f, 0.4f, 0.9f, 0.5f), new Vector3(0f, 1.5f, 0f), new Vector3(5f, 3f, 5f), Vector3.back, transform); 15 | } 16 | 17 | public override void InitializeComponent() 18 | { 19 | ObjectReferences.ResetPoint.transform.position = transform.position; 20 | var t = new GameObject(); 21 | t.transform.position = transform.position + new Vector3(0f, 1.8f, 7.5f); 22 | ObjectReferences.ManagerDonor.ScoreDisplayPoint = t.transform; 23 | 24 | Destroy(this); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /WurstMod/MappingComponents/TakeAndHold/TNH_DestructibleBarrierPoint.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using UnityEngine; 4 | using WurstMod.Runtime; 5 | using WurstMod.Shared; 6 | 7 | namespace WurstMod.MappingComponents.TakeAndHold 8 | { 9 | public class TNH_DestructibleBarrierPoint : ComponentProxy 10 | { 11 | [Tooltip("All cover points which become active when this barrier exists.")] 12 | public List CoverPoints; 13 | 14 | private void OnDrawGizmos() 15 | { 16 | Extensions.GenericGizmoCube(new Color(0.0f, 0.6f, 0.0f, 0.5f), new Vector3(0, 1, 0), new Vector3(1, 2, 0.1f), Vector3.zero, transform); 17 | } 18 | 19 | public override int ResolveOrder => 2; 20 | 21 | public override void InitializeComponent() 22 | { 23 | FistVR.TNH_DestructibleBarrierPoint real = gameObject.AddComponent(); 24 | 25 | real.Obstacle = gameObject.GetComponent(); 26 | real.CoverPoints = real.GetComponentsInChildren(true).ToList(); 27 | real.BarrierDataSets = new List(); 28 | 29 | for (int ii = 0; ii < 2; ii++) 30 | { 31 | FistVR.TNH_DestructibleBarrierPoint.BarrierDataSet barrierSet = new FistVR.TNH_DestructibleBarrierPoint.BarrierDataSet(); 32 | barrierSet.BarrierPrefab = ObjectReferences.BarrierDonor.BarrierDataSets[ii].BarrierPrefab; 33 | barrierSet.Points = new List(); 34 | 35 | real.BarrierDataSets.Add(barrierSet); 36 | } 37 | 38 | // This should only be run in the editor, but OH WELL. 39 | real.BakePoints(); 40 | 41 | Destroy(this); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /WurstMod/MappingComponents/TakeAndHold/TNH_HoldPoint.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using UnityEngine; 4 | using WurstMod.Runtime; 5 | using WurstMod.Shared; 6 | 7 | #if UNITY_EDITOR 8 | using WurstMod.UnityEditor; 9 | #endif 10 | 11 | namespace WurstMod.MappingComponents.TakeAndHold 12 | { 13 | public class TNH_HoldPoint : ComponentProxy 14 | { 15 | [Tooltip("A DISABLED mesh (usually a cube) that encompasses the entire Hold Point. No collider.")] 16 | public List Bounds; 17 | 18 | [Tooltip("DISABLED parent object with DISABLED children. These become active during a Hold, and have colliders on the NavBlock layer to prevent the player from leaving the hold area. Place collider objects on the entrances to a hold point.")] 19 | public GameObject NavBlockers; 20 | 21 | [Tooltip("PARENT. Parent object for all barriers. Place children of this object on the floor.")] 22 | public Transform BarrierPoints; 23 | 24 | [Tooltip("PARENT. Parent object for cover points. Cover points not related to Barriers should be a child of this object.")] 25 | public Transform CoverPoints; 26 | 27 | [Tooltip("System Node spawns here. Place on floor.")] 28 | public Transform SpawnPoint_SystemNode; 29 | 30 | [Tooltip("PARENT. Encryption spawns in the positions of this object's children. Make a bunch, place in air.")] 31 | public Transform SpawnPoints_Targets; 32 | 33 | [Tooltip("PARENT. Unused?")] 34 | public Transform SpawnPoints_Turrets; 35 | 36 | [Tooltip("PARENT. Sosigs will choose from this object's AttackVector children when spawning in during a hold.")] 37 | public Transform AttackVectors; 38 | 39 | [Tooltip("PARENT. Sosigs will spawn in the positions of this object's children when this hold point becomes takeable. Place on floor.")] 40 | public Transform SpawnPoints_Sosigs_Defense; 41 | 42 | 43 | private void OnDrawGizmos() 44 | { 45 | if (Bounds != null) Extensions.GenericGizmoCubeOutline(Color.white, Vector3.zero, Vector3.one, Bounds.ToArray()); 46 | if (SpawnPoint_SystemNode != null) Extensions.GenericGizmoSphere(new Color(0.0f, 0.8f, 0.8f), 1.5f * Vector3.up, 0.25f, SpawnPoint_SystemNode); 47 | if (SpawnPoints_Sosigs_Defense != null) Extensions.GenericGizmoSphere(new Color(0.8f, 0f, 0f, 0.5f), Vector3.zero, 0.25f, SpawnPoints_Sosigs_Defense.AsEnumerable().ToArray()); 48 | if (SpawnPoints_Turrets != null) Extensions.GenericGizmoSphere(new Color(0.8f, 0f, 0f, 0.1f), Vector3.zero, 0.25f, SpawnPoints_Turrets.AsEnumerable().ToArray()); 49 | if (SpawnPoints_Targets != null) Extensions.GenericGizmoCube(new Color(1f, 0.0f, 0.0f, 0.5f), Vector3.zero, new Vector3(0.1f, 0.5f, 0.1f), Vector3.zero, SpawnPoints_Targets.AsEnumerable().ToArray()); 50 | } 51 | 52 | public override void InitializeComponent() 53 | { 54 | FistVR.TNH_HoldPoint real = gameObject.AddComponent(); 55 | 56 | real.M = ObjectReferences.ManagerDonor; 57 | real.Bounds = Bounds; 58 | real.NavBlockers = NavBlockers; 59 | real.BarrierPoints = BarrierPoints.AsEnumerable().Select(x => x.gameObject.GetComponent()).ToList(); 60 | real.CoverPoints = CoverPoints.AsEnumerable().Select(x => x.gameObject.GetComponent()).ToList(); 61 | real.SpawnPoint_SystemNode = SpawnPoint_SystemNode; 62 | real.SpawnPoints_SystemNode = new List(); 63 | real.SpawnPoints_Targets = SpawnPoints_Targets.AsEnumerable().ToList(); 64 | real.SpawnPoints_Turrets = SpawnPoints_Turrets.AsEnumerable().ToList(); 65 | real.AttackVectors = AttackVectors.AsEnumerable().Select(x => Resolve_AttackVector(x.GetComponent())).Cast().ToList(); 66 | real.SpawnPoints_Sosigs_Defense = SpawnPoints_Sosigs_Defense.AsEnumerable().ToList(); 67 | 68 | FistVR.AudioEvent wave = new FistVR.AudioEvent(); 69 | wave.Clips = ObjectReferences.HoldPointDonor.AUDEvent_HoldWave.Clips.ToList(); 70 | wave.VolumeRange = new Vector2(0.4f, 0.4f); 71 | wave.PitchRange = new Vector2(1, 1); 72 | wave.ClipLengthRange = new Vector2(1, 1); 73 | 74 | FistVR.AudioEvent success = new FistVR.AudioEvent(); 75 | success.Clips = ObjectReferences.HoldPointDonor.AUDEvent_Success.Clips.ToList(); 76 | success.VolumeRange = new Vector2(0.4f, 0.4f); 77 | success.PitchRange = new Vector2(1, 1); 78 | success.ClipLengthRange = new Vector2(1, 1); 79 | 80 | FistVR.AudioEvent failure = new FistVR.AudioEvent(); 81 | failure.Clips = ObjectReferences.HoldPointDonor.AUDEvent_Failure.Clips.ToList(); 82 | failure.VolumeRange = new Vector2(0.4f, 0.4f); 83 | failure.PitchRange = new Vector2(1, 1); 84 | failure.ClipLengthRange = new Vector2(1, 1); 85 | 86 | real.AUDEvent_HoldWave = wave; 87 | real.AUDEvent_Success = success; 88 | real.AUDEvent_Failure = failure; 89 | real.VFX_HoldWave = ObjectReferences.HoldPointDonor.VFX_HoldWave; 90 | 91 | Destroy(this); 92 | } 93 | 94 | 95 | private object Resolve_AttackVector(AttackVector proxy) 96 | { 97 | GameObject owner = proxy.gameObject; 98 | FistVR.TNH_HoldPoint.AttackVector real = new FistVR.TNH_HoldPoint.AttackVector(); 99 | 100 | real.SpawnPoints_Sosigs_Attack = proxy.SpawnPoints_Sosigs_Attack; 101 | real.GrenadeVector = proxy.GrenadeVector; 102 | real.GrenadeRandAngle = proxy.GrenadeRandAngle; 103 | real.GrenadeVelRange = proxy.GrenadeVelRange; 104 | 105 | return real; 106 | } 107 | 108 | #if UNITY_EDITOR 109 | public override void OnExport(ExportErrors err) 110 | { 111 | // Make sure the nav blockers are disabled 112 | NavBlockers.SetActive(false); 113 | 114 | // Make sure we have at least 9 defense points 115 | if (SpawnPoints_Sosigs_Defense.AsEnumerable().Count() < 9) 116 | { 117 | err.AddError("Holds must have at least 9 Sosig Defense Spawn Points.", this); 118 | } 119 | } 120 | #endif 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /WurstMod/MappingComponents/TakeAndHold/TNH_SupplyPoint.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using UnityEngine; 3 | using WurstMod.Runtime; 4 | using WurstMod.Shared; 5 | 6 | namespace WurstMod.MappingComponents.TakeAndHold 7 | { 8 | public class TNH_SupplyPoint : ComponentProxy 9 | { 10 | [Tooltip("A DISABLED mesh (usually a cube) that encompasses the entire Supply Point. No collider.")] 11 | public Transform Bounds; 12 | 13 | [Tooltip("PARENT. Parent for CoverPoints which are not a part of Barriers. Rotation does not matter for CoverPoints, and they should be placed on the floor.")] 14 | public Transform CoverPoints; 15 | 16 | [Tooltip("If the player spawns on this Supply Point, they will spawn here, facing Forward (+Z).")] 17 | public Transform SpawnPoint_PlayerSpawn; 18 | 19 | [Tooltip("PARENT. When this supply point spawns between waves, defenders will use the child positions of this object to spawn.")] 20 | public Transform SpawnPoints_Sosigs_Defense; 21 | 22 | [Tooltip("PARENT. I'm not sure these are used right now, but feel free to scatter them around.")] 23 | public Transform SpawnPoints_Turrets; 24 | 25 | [Tooltip("PARENT. Shop panels spawn at this object's children positions. Most Supply Points have 3-5, so I'd say at least 3. Place them on the ground with +Z facing where the player would stand to use the shop.")] 26 | public Transform SpawnPoints_Panels; 27 | 28 | [Tooltip("PARENT. Breakable white boxes will spawn at this object's children positions.")] 29 | public Transform SpawnPoints_Boxes; 30 | 31 | [Tooltip("PARENT. If the player spawns here, the child positions points are where tables will spawn with the initial items. Usually two tables. Place them on the ground.")] 32 | public Transform SpawnPoint_Tables; 33 | 34 | [Tooltip("Case position for when player spawns here. Place it about 0.8 units above the table for it to sit properly.")] 35 | public Transform SpawnPoint_CaseLarge; 36 | 37 | [Tooltip("Case position for when player spawns here. Place it about 0.8 units above the table for it to sit properly.")] 38 | public Transform SpawnPoint_CaseSmall; 39 | 40 | [Tooltip("Melee weapon position for when player spawns here. Place it about 1.1 units above the table.")] 41 | public Transform SpawnPoint_Melee; 42 | 43 | [Tooltip("PARENT. Small item(s) position for when player spawns here. Place 3 or more of them about 1 unit above the table.")] 44 | public Transform SpawnPoints_SmallItem; 45 | 46 | [Tooltip("Shield position for when player spawns here. Is this even implemented?")] 47 | public Transform SpawnPoint_Shield; 48 | 49 | 50 | 51 | 52 | void OnDrawGizmos() 53 | { 54 | if (Bounds != null) Extensions.GenericGizmoCubeOutline(Color.white, Vector3.zero, Vector3.one, Bounds); 55 | if (SpawnPoints_Panels != null) Extensions.GenericGizmoCube(new Color(0.4f, 0.4f, 0.9f, 0.5f), new Vector3(0f, 1.5f, 0.25f), new Vector3(2.3f, 1.2f, 0.5f), Vector3.forward, SpawnPoints_Panels.AsEnumerable().ToArray()); 56 | if (SpawnPoint_CaseLarge != null) Extensions.GenericGizmoCube(new Color(1f, 0.4f, 0.0f, 0.5f), new Vector3(0f, 0.12f, 0f), new Vector3(1.4f, 0.24f, 0.35f), Vector3.forward, SpawnPoint_CaseLarge); 57 | if (SpawnPoint_CaseSmall != null) Extensions.GenericGizmoCube(new Color(1f, 0.4f, 0.0f, 0.5f), new Vector3(0f, 0.12f, 0f), new Vector3(0.6f, 0.24f, 0.35f), Vector3.forward, SpawnPoint_CaseSmall); 58 | if (SpawnPoint_Tables != null) Extensions.GenericGizmoCube(new Color(0.5f, 0.5f, 0.5f, 0.5f), new Vector3(0f, 0.4f, 0.1f), new Vector3(0.7f, 0.8f, 1.5f), Vector3.zero, SpawnPoint_Tables.AsEnumerable().ToArray()); 59 | if (SpawnPoints_Boxes != null) Extensions.GenericGizmoCube(new Color(0.7f, 0.7f, 0.7f, 0.5f), Vector3.zero, 0.5f * Vector3.one, Vector3.zero, SpawnPoints_Boxes.AsEnumerable().ToArray()); 60 | if (SpawnPoints_Sosigs_Defense != null) Extensions.GenericGizmoSphere(new Color(0.8f, 0f, 0f, 0.5f), Vector3.zero, 0.25f, SpawnPoints_Sosigs_Defense.AsEnumerable().ToArray()); 61 | if (SpawnPoints_Turrets != null) Extensions.GenericGizmoSphere(new Color(0.8f, 0f, 0f, 0.1f), Vector3.zero, 0.25f, SpawnPoints_Turrets.AsEnumerable().ToArray()); 62 | if (SpawnPoint_Melee != null) Extensions.GenericGizmoSphere(new Color(0.0f, 0.8f, 0f, 0.5f), Vector3.zero, 0.2f, SpawnPoint_Melee); 63 | if (SpawnPoints_SmallItem != null) Extensions.GenericGizmoSphere(new Color(0.0f, 0.8f, 0f, 0.5f), Vector3.zero, 0.1f, SpawnPoints_SmallItem.AsEnumerable().ToArray()); 64 | if (SpawnPoint_PlayerSpawn != null) Extensions.GenericGizmoSphere(new Color(0.0f, 0.8f, 0.8f, 0.5f), Vector3.zero, 0.25f, SpawnPoint_PlayerSpawn); 65 | if (SpawnPoint_Shield != null) Extensions.GenericGizmoCube(new Color(0.0f, 0.8f, 0.0f, 0.1f), Vector3.zero, new Vector3(0.4f, 0.6f, 0.1f), Vector3.zero, SpawnPoint_Shield); 66 | } 67 | 68 | public override void InitializeComponent() 69 | { 70 | FistVR.TNH_SupplyPoint real = gameObject.AddComponent(); 71 | 72 | real.M = ObjectReferences.ManagerDonor; 73 | real.Bounds = Bounds; 74 | real.CoverPoints = CoverPoints.AsEnumerable().Select(x => x.gameObject.GetComponent()).ToList(); 75 | real.SpawnPoint_PlayerSpawn = SpawnPoint_PlayerSpawn; 76 | real.SpawnPoints_Sosigs_Defense = SpawnPoints_Sosigs_Defense.AsEnumerable().ToList(); 77 | real.SpawnPoints_Turrets = SpawnPoints_Turrets.AsEnumerable().ToList(); 78 | real.SpawnPoints_Panels = SpawnPoints_Panels.AsEnumerable().ToList(); 79 | real.SpawnPoints_Boxes = SpawnPoints_Boxes.AsEnumerable().ToList(); 80 | real.SpawnPoint_Tables = SpawnPoint_Tables.AsEnumerable().ToList(); 81 | real.SpawnPoint_CaseLarge = SpawnPoint_CaseLarge; 82 | real.SpawnPoint_CaseSmall = SpawnPoint_CaseSmall; 83 | real.SpawnPoint_Melee = SpawnPoint_Melee; 84 | real.SpawnPoints_SmallItem = SpawnPoints_SmallItem.AsEnumerable().ToList(); 85 | real.SpawnPoint_Shield = SpawnPoint_Shield; 86 | 87 | Destroy(this); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /WurstMod/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("WurstMod")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("WurstMod")] 13 | [assembly: AssemblyCopyright("Copyright © 2020")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("43e34e28-2c7e-42f4-92ca-c731aecff593")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("2.0.0.0")] 36 | [assembly: AssemblyFileVersion("2.0.0.0")] 37 | -------------------------------------------------------------------------------- /WurstMod/Runtime/CustomLevelFinder.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | using WurstMod.Shared; 5 | 6 | namespace WurstMod.Runtime 7 | { 8 | public static class CustomLevelFinder 9 | { 10 | public static readonly List ArchiveLevels = new List(); 11 | 12 | /// 13 | /// Iterates over the custom levels found in the custom levels directory 14 | /// 15 | public static IEnumerable EnumerateLevelInfos() 16 | { 17 | foreach (var level in ArchiveLevels) yield return level; 18 | } 19 | 20 | /// 21 | /// Returns an array of all the found level infos. Use this if you need to iterate the array multiple times 22 | /// 23 | public static LevelInfo[] GetLevelInfos() => EnumerateLevelInfos().ToArray(); 24 | } 25 | } -------------------------------------------------------------------------------- /WurstMod/Runtime/Entrypoint.cs: -------------------------------------------------------------------------------- 1 | #if !UNITY_EDITOR 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Reflection; 5 | using BepInEx.Configuration; 6 | using BepInEx.Logging; 7 | using Deli; 8 | using Deli.Setup; 9 | using Deli.VFS; 10 | using Steamworks; 11 | using UnityEngine; 12 | using UnityEngine.SceneManagement; 13 | using Valve.VR; 14 | using WurstMod.Runtime.ScenePatchers; 15 | using WurstMod.Shared; 16 | using Constants = WurstMod.Shared.Constants; 17 | 18 | namespace WurstMod.Runtime 19 | { 20 | public class Entrypoint : DeliBehaviour 21 | { 22 | private static readonly Dictionary Assemblies = new Dictionary(); 23 | private static ConfigEntry loadDebugLevels; 24 | private static ConfigEntry useLegacyLoadingMethod; 25 | 26 | public Entrypoint() 27 | { 28 | int buildId = -1; 29 | try 30 | { 31 | SteamAPI.Init(); 32 | buildId = SteamApps.GetAppBuildId(); 33 | } 34 | catch (InvalidOperationException) 35 | { 36 | // Ignored 37 | } 38 | 39 | if (buildId < 7036061) 40 | throw new Exception("This version of the game is not supported. Please update your game."); 41 | 42 | // Patches 43 | Patches.Patch(); 44 | 45 | // Add a number of callbacks 46 | Stages.Setup += StagesOnSetup; 47 | SceneManager.sceneLoaded += SceneManager_sceneLoaded; 48 | AppDomain.CurrentDomain.AssemblyLoad += (sender, e) => { Assemblies[e.LoadedAssembly.FullName] = e.LoadedAssembly; }; 49 | AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => 50 | { 51 | Assemblies.TryGetValue(e.Name, out var assembly); 52 | return assembly; 53 | }; 54 | 55 | // Config values 56 | loadDebugLevels = Config.Bind("Debug", "LoadDebugLevels", false, "True if you want the included default levels to be loaded"); 57 | useLegacyLoadingMethod = Config.Bind("Debug", "UseLegacyLoadingMethod", false, $"True if you want to support loading legacy v1 or standalone levels from the {Constants.LegacyLevelsDirectory} folder"); 58 | 59 | // Legacy support 60 | if (useLegacyLoadingMethod.Value) 61 | LegacySupport.EnsureLegacyFolderExists(Resources.GetFile("legacyManifest.json")); 62 | } 63 | 64 | private void StagesOnSetup(SetupStage stage) 65 | { 66 | stage.SharedAssetLoaders[Source, "level"] = LevelLoader; 67 | } 68 | 69 | private void LevelLoader(Stage stage, Mod mod, IHandle handle) 70 | { 71 | // If the config has disabled loading the default included levels, return 72 | if (!loadDebugLevels.Value && mod.Info.Guid == "wurstmodders.wurstmod") 73 | return; 74 | 75 | // If this is the legacy stuff and we have that disabled, return 76 | if (!useLegacyLoadingMethod.Value && mod.Info.Guid == "wurstmodders.wurstmod.legacy") 77 | return; 78 | 79 | // Try to make a level info from it 80 | var level = LevelInfo.FromFrameworkMod(mod, handle); 81 | 82 | if (!level.HasValue) 83 | { 84 | // Skip an error here because this will throw some errors on folders we know are invalid. 85 | if (mod.Info.Guid != "wurstmodders.wurstmod.legacy") 86 | Debug.LogError($"Level in {mod}, {handle} is not valid!"); 87 | } 88 | else 89 | { 90 | CustomLevelFinder.ArchiveLevels.Add(level.Value); 91 | Logger.LogInfo($"Discovered level {level.Value.SceneName} in {mod}, {handle}"); 92 | } 93 | } 94 | 95 | private void SceneManager_sceneLoaded(Scene scene, LoadSceneMode mode) 96 | { 97 | TNH_LevelSelector.SetupLevelSelector(scene); 98 | Generic_LevelPopulator.SetupLevelPopulator(scene); 99 | StartCoroutine(Loader.OnSceneLoad(scene)); 100 | } 101 | } 102 | } 103 | #endif -------------------------------------------------------------------------------- /WurstMod/Runtime/LegacySupport.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | #if !UNITY_EDITOR 5 | using Deli.VFS; 6 | #endif 7 | using UnityEngine; 8 | using WurstMod.Shared; 9 | 10 | namespace WurstMod.Runtime 11 | { 12 | public class LegacySupport 13 | { 14 | // I'm VERY explicit with namespaces in this file because strange things can happen if a stray using 15 | // gets thrown in while working with classes of the same name like this. 16 | // Not required, but it feels more sane (albeit verbose.) 17 | 18 | // The following arrays map the old enum ordering to the new hash-based future-proof ordering. 19 | public static readonly int[] pMatLegacyMapping = new int[] 20 | { 21 | -1389548697, 22 | 339799954, 23 | -1592592720, 24 | -1228112709, 25 | -1170414547, 26 | -1989712078, 27 | 2012074650, 28 | 1273286112, 29 | -1234456471, 30 | -1946916951, 31 | -725349196, 32 | 1686857538, 33 | -65249002, 34 | -955345693, 35 | 526415300, 36 | -2059344742, 37 | 746711929, 38 | -858280334, 39 | 1001966363, 40 | 1428605659, 41 | -728703673, 42 | 1178975115, 43 | -1261570224, 44 | -1261569134, 45 | -751390192, 46 | -466768857, 47 | 1400949664, 48 | -158744292, 49 | -1788895460, 50 | -195832731, 51 | 324251733, 52 | -1698391924, 53 | 1984043213, 54 | 370905090, 55 | -566556950, 56 | 1287532213, 57 | 1273826288, 58 | -2146462032, 59 | 762136440, 60 | 1505815014, 61 | -582586173, 62 | -724419178, 63 | 255480758, 64 | 1556236128, 65 | 624679819, 66 | 660109706, 67 | -2146336943, 68 | -1779147911, 69 | -151837501 70 | }; 71 | 72 | public static readonly int[] matDefLegacyMapping = new int[] 73 | { 74 | 1752877821, 75 | -1347286364, 76 | 644301452, 77 | -1669265951, 78 | -871205235, 79 | -635266500, 80 | 332398969, 81 | 1273826288, 82 | 1985703140, 83 | 1729971855, 84 | -1675716207, 85 | 1942859388, 86 | -1487240928, 87 | 383750667, 88 | 432136527, 89 | 1211416655, 90 | -34193051, 91 | 1943035998, 92 | -1149654923, 93 | -649847075, 94 | 610581220, 95 | 942411018, 96 | 2144447471, 97 | -1896619314, 98 | 1242992293, 99 | -134865591, 100 | 1827179403, 101 | 1461872896, 102 | 1417815362, 103 | -1779147911, 104 | -1661987665, 105 | -1942452169, 106 | 757445278, 107 | 1483923932, 108 | -1690371124, 109 | 1213917816, 110 | 434755139, 111 | 1335110134, 112 | 1960591488, 113 | 666790754, 114 | -1853667375, 115 | -321340768, 116 | 1725752409, 117 | -529192027, 118 | 1454812933, 119 | -213122109, 120 | 892418936, 121 | -1898909600, 122 | -1258062406, 123 | -1217966965, 124 | 761218943, 125 | 1081916419, 126 | -1271050433, 127 | 10600013, 128 | -1779142660 129 | }; 130 | 131 | #if !UNITY_EDITOR 132 | public static void EnsureLegacyFolderExists(IFileHandle legacyManifest) 133 | { 134 | if (legacyManifest is null) 135 | { 136 | return; 137 | } 138 | 139 | var manifest = Path.Combine(Constants.LegacyLevelsDirectory, "manifest.json"); 140 | if (File.Exists(manifest)) return; 141 | Directory.CreateDirectory(Constants.LegacyLevelsDirectory); 142 | Directory.CreateDirectory(Path.Combine(Constants.LegacyLevelsDirectory, "TakeAndHold")); 143 | Directory.CreateDirectory(Path.Combine(Constants.LegacyLevelsDirectory, "Other")); 144 | 145 | var stream = legacyManifest.OpenRead(); 146 | var buffer = new byte[stream.Length]; 147 | stream.Read(buffer, 0, buffer.Length); 148 | File.WriteAllBytes(manifest, buffer); 149 | } 150 | #endif 151 | } 152 | } 153 | 154 | // We can maintain backwards compatibility like this. It's a little cheesy of course, but relatively unobtrusive. 155 | namespace WurstMod.TNH 156 | { 157 | [Obsolete] 158 | [AddComponentMenu("")] 159 | public class AICoverPoint : WurstMod.MappingComponents.Generic.AICoverPoint 160 | { 161 | } 162 | 163 | [Obsolete] 164 | [AddComponentMenu("")] 165 | public class AttackVector : WurstMod.MappingComponents.TakeAndHold.AttackVector 166 | { 167 | } 168 | 169 | [Obsolete] 170 | [AddComponentMenu("")] 171 | public class FVRHandGrabPoint : WurstMod.MappingComponents.Generic.FVRHandGrabPoint 172 | { 173 | } 174 | 175 | [Obsolete] 176 | [AddComponentMenu("")] 177 | public class FVRReverbEnvironment : WurstMod.MappingComponents.Generic.FVRReverbEnvironment 178 | { 179 | } 180 | 181 | [Obsolete] 182 | [AddComponentMenu("")] 183 | public class PMat : WurstMod.MappingComponents.Generic.PMat 184 | { 185 | void Awake() 186 | { 187 | def = (WurstMod.Shared.ResourceDefs.PMat) WurstMod.Runtime.LegacySupport.pMatLegacyMapping[(int) def]; 188 | matDef = 189 | (WurstMod.Shared.ResourceDefs.MatDef) WurstMod.Runtime.LegacySupport.matDefLegacyMapping[(int) matDef]; 190 | } 191 | } 192 | 193 | [Obsolete] 194 | [AddComponentMenu("")] 195 | public class ScoreboardArea : WurstMod.MappingComponents.TakeAndHold.ScoreboardArea 196 | { 197 | } 198 | 199 | [Obsolete] 200 | [AddComponentMenu("")] 201 | public class TNH_DestructibleBarrierPoint : WurstMod.MappingComponents.TakeAndHold.TNH_DestructibleBarrierPoint 202 | { 203 | } 204 | 205 | [Obsolete] 206 | [AddComponentMenu("")] 207 | public class TNH_HoldPoint : WurstMod.MappingComponents.TakeAndHold.TNH_HoldPoint 208 | { 209 | } 210 | 211 | [Obsolete] 212 | [AddComponentMenu("")] 213 | public class TNH_Level : WurstMod.MappingComponents.Generic.CustomScene 214 | { 215 | public Material skybox; 216 | 217 | void Awake() 218 | { 219 | Skybox = skybox; 220 | } 221 | } 222 | 223 | [Obsolete] 224 | [AddComponentMenu("")] 225 | public class TNH_SupplyPoint : WurstMod.MappingComponents.TakeAndHold.TNH_SupplyPoint 226 | { 227 | } 228 | } 229 | 230 | namespace WurstMod.TNH.Extras 231 | { 232 | [Obsolete] 233 | [AddComponentMenu("")] 234 | public class ForcedSpawn : WurstMod.MappingComponents.TakeAndHold.ForcedSpawn 235 | { 236 | } 237 | } 238 | 239 | namespace WurstMod.Generic 240 | { 241 | [Obsolete] 242 | [AddComponentMenu("")] 243 | public class GenericPrefab : WurstMod.MappingComponents.Sandbox.GenericPrefab 244 | { 245 | } 246 | 247 | [Obsolete] 248 | [AddComponentMenu("")] 249 | public class GroundPanel : WurstMod.MappingComponents.Sandbox.GroundPanel 250 | { 251 | } 252 | 253 | [Obsolete] 254 | [AddComponentMenu("")] 255 | public class ItemSpawner : WurstMod.MappingComponents.Sandbox.GenericPrefab 256 | { 257 | void Awake() 258 | { 259 | objectType = MappingComponents.Sandbox.Prefab.ItemSpawner; 260 | } 261 | } 262 | 263 | [Obsolete] 264 | [AddComponentMenu("")] 265 | public class PointableButton : WurstMod.MappingComponents.Sandbox.PointableButton 266 | { 267 | } 268 | 269 | [Obsolete] 270 | [AddComponentMenu("")] 271 | public class Spawn : WurstMod.MappingComponents.Sandbox.Spawn 272 | { 273 | } 274 | } 275 | 276 | namespace WurstMod.Any 277 | { 278 | [Obsolete] 279 | [AddComponentMenu("")] 280 | public class AICoverPoint : WurstMod.MappingComponents.Generic.AICoverPoint 281 | { 282 | } 283 | 284 | [Obsolete] 285 | [AddComponentMenu("")] 286 | public class AnvilPrefab : WurstMod.MappingComponents.Generic.AnvilPrefab 287 | { 288 | public string Guid; 289 | public string Bundle; 290 | public string AssetName; 291 | 292 | void Awake() 293 | { 294 | spawnOnSceneLoad = true; 295 | 296 | string[] enumNames = Enum.GetNames(typeof(WurstMod.Shared.ResourceDefs.AnvilAsset)); 297 | string correctEnumName = enumNames.Where(x => x.EndsWith(AssetName)).FirstOrDefault(); 298 | if (!string.IsNullOrEmpty(correctEnumName)) 299 | { 300 | prefab = (WurstMod.Shared.ResourceDefs.AnvilAsset) Enum.Parse( 301 | typeof(WurstMod.Shared.ResourceDefs.AnvilAsset), correctEnumName); 302 | } 303 | } 304 | } 305 | 306 | [Obsolete] 307 | [AddComponentMenu("")] 308 | public class FVRHandGrabPoint : WurstMod.MappingComponents.Generic.FVRHandGrabPoint 309 | { 310 | } 311 | 312 | [Obsolete] 313 | [AddComponentMenu("")] 314 | public class FVRReverbEnvironment : WurstMod.MappingComponents.Generic.FVRReverbEnvironment 315 | { 316 | } 317 | 318 | [Obsolete] 319 | [AddComponentMenu("")] 320 | public class PMat : WurstMod.MappingComponents.Generic.PMat 321 | { 322 | void Awake() 323 | { 324 | def = (WurstMod.Shared.ResourceDefs.PMat) WurstMod.Runtime.LegacySupport.pMatLegacyMapping[(int) def]; 325 | matDef = 326 | (WurstMod.Shared.ResourceDefs.MatDef) WurstMod.Runtime.LegacySupport.matDefLegacyMapping[(int) matDef]; 327 | } 328 | } 329 | 330 | [Obsolete] 331 | [AddComponentMenu("")] 332 | public class Target : WurstMod.MappingComponents.Generic.Target 333 | { 334 | } 335 | 336 | [Obsolete] 337 | [AddComponentMenu("")] 338 | public class Trigger : WurstMod.MappingComponents.Generic.Trigger 339 | { 340 | } 341 | } -------------------------------------------------------------------------------- /WurstMod/Runtime/Loader.cs: -------------------------------------------------------------------------------- 1 | #if !UNITY_EDITOR 2 | using System; 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Reflection; 8 | using Deli.VFS; 9 | using HarmonyLib; 10 | using UnityEngine; 11 | using UnityEngine.SceneManagement; 12 | using WurstMod.MappingComponents; 13 | using WurstMod.MappingComponents.Generic; 14 | using WurstMod.Shared; 15 | using Object = UnityEngine.Object; 16 | 17 | namespace WurstMod.Runtime 18 | { 19 | /// 20 | /// WurstMod loading class. Responsible for loading a map. 21 | /// 22 | public static class Loader 23 | { 24 | public static bool IsLoadInProgress; 25 | 26 | /// 27 | /// This is called by the BepInEx entrypoint. It is fired when ANY scene loads, including vanilla ones. 28 | /// 29 | public static IEnumerator OnSceneLoad(Scene scene) 30 | { 31 | if (!IsLoadInProgress && LevelToLoad.HasValue) 32 | { 33 | // Check if this scene is the one we wanted to load for this level 34 | var level = LevelToLoad.Value; 35 | var sceneLoader = CustomSceneLoader.GetSceneLoaderForGamemode(level.Gamemode); 36 | 37 | // If either of these are true something is wrong and we shouldn't try to load anything 38 | if (sceneLoader is null || sceneLoader.BaseScene != scene.name) yield break; 39 | 40 | // We're loading the base scene for a modded scene 41 | IsLoadInProgress = true; 42 | 43 | // Find references 44 | ObjectReferences.FindReferences(scene); 45 | 46 | // Load the level 47 | yield return LoadCustomScene(level, sceneLoader); 48 | } 49 | 50 | if (IsLoadInProgress && LevelToLoad.HasValue) 51 | { 52 | IsLoadInProgress = false; 53 | } 54 | } 55 | 56 | /// 57 | /// This method is called to load a custom scene 58 | /// 59 | private static IEnumerator LoadCustomScene(LevelInfo level, CustomSceneLoader sceneLoader) 60 | { 61 | // Step 0: Get the loaded scene and find any custom loaders and scene patchers 62 | _loadedScene = SceneManager.GetActiveScene(); 63 | if (sceneLoader == null) 64 | { 65 | Debug.LogError($"No SceneLoader found for the gamemode '{level.Gamemode}'! Cannot load level."); 66 | yield break; 67 | } 68 | 69 | // Step 1: Let the custom loaders do their PRELOAD method 70 | sceneLoader.PreLoad(); 71 | 72 | // Step 2: Disable all currently active objects. 73 | var disabledObjects = new List(); 74 | var objects = _loadedScene.GetRootGameObjects(); 75 | foreach (var gameObject in objects) 76 | { 77 | if (!gameObject.activeSelf) continue; 78 | disabledObjects.Add(gameObject); 79 | gameObject.SetActive(false); 80 | } 81 | 82 | // Step 3: Destroy all unused objects 83 | foreach (var gameObject in objects.SelectMany(o => o.GetComponentsInChildren())) 84 | foreach (var filter in sceneLoader.EnumerateDestroyOnLoad()) 85 | if (gameObject.name.Contains(filter)) 86 | Object.Destroy(gameObject.gameObject); 87 | 88 | // Step 4: Load the modded scene and merge it into the loaded scene 89 | yield return MergeCustomScene(level); 90 | var loadedRoot = ObjectReferences.CustomScene; 91 | sceneLoader.LevelRoot = loadedRoot; 92 | 93 | // Step 5: Resolve component proxies and the scene loader to do what it needs to do 94 | foreach (var proxy in loadedRoot.GetComponentsInChildren().OrderByDescending(x => x.ResolveOrder)) 95 | { 96 | try 97 | { 98 | proxy.InitializeComponent(); 99 | } 100 | catch (Exception e) 101 | { 102 | Debug.LogError("Error initializing component " + proxy + ":" + e.Message + "\n" + e.StackTrace); 103 | } 104 | } 105 | sceneLoader.Resolve(); 106 | 107 | // Step 6: Re-enable disabled objects 108 | foreach (var gameObject in disabledObjects.Where(gameObject => gameObject)) gameObject.SetActive(true); 109 | 110 | // Step 7: Let the custom loaders do their POSTLOAD method 111 | sceneLoader.PostLoad(); 112 | } 113 | 114 | /// 115 | /// This method will load the custom scene and merge it in. 116 | /// 117 | private static IEnumerator MergeCustomScene(LevelInfo level) 118 | { 119 | // If we've already loaded the bundle, take it from the dict, otherwise load it. 120 | AssetBundle bundle; 121 | if (!LoadedBundles.ContainsKey(level.AssetBundlePath)) 122 | { 123 | bundle = level.AssetBundle; 124 | LoadedBundles.Add(level.AssetBundlePath, bundle); 125 | } 126 | else bundle = LoadedBundles[level.AssetBundlePath]; 127 | 128 | var sceneName = Path.GetFileNameWithoutExtension(bundle.GetAllScenePaths()[0]); 129 | 130 | // Let Unity load the custom scene 131 | SceneManager.LoadScene(sceneName, LoadSceneMode.Additive); 132 | yield return null; 133 | 134 | // Then merge the scene into the original 135 | SceneManager.MergeScenes(SceneManager.GetSceneByName(sceneName), SceneManager.GetActiveScene()); 136 | 137 | // Find the level component 138 | ObjectReferences.CustomScene = _loadedScene.GetRootGameObjects() 139 | .Single(x => x.name == "[TNHLEVEL]" || x.name == "[LEVEL]") 140 | .GetComponent(); 141 | } 142 | 143 | // Reference to the currently loaded scene 144 | private static Scene _loadedScene; 145 | 146 | // Keep track of which assemblies and asset bundles we've already loaded 147 | private static readonly Dictionary LoadedBundles = new Dictionary(); 148 | private static readonly Dictionary LoadedAssemblies = new Dictionary(); 149 | 150 | // Public field to set which level we'll load 151 | public static LevelInfo? LevelToLoad = null; 152 | } 153 | } 154 | #endif -------------------------------------------------------------------------------- /WurstMod/Runtime/ModdedLevelInfo.cs: -------------------------------------------------------------------------------- 1 | using FistVR; 2 | using WurstMod.Shared; 3 | 4 | namespace WurstMod.Runtime 5 | { 6 | public class ModdedLevelInfo : TNH_UIManager.LevelData 7 | { 8 | public ModdedLevelInfo(LevelInfo level) 9 | { 10 | #if !UNITY_EDITOR 11 | IsModLevel = true; 12 | LevelAuthor = level.Author; 13 | LevelDescription = level.Description; 14 | LevelDisplayName = level.SceneName; 15 | LevelID = level.Identifier; 16 | LevelImage = level.Sprite; 17 | Original = level; 18 | LevelSceneName = "TakeAndHoldClassic"; 19 | #endif 20 | } 21 | 22 | public LevelInfo Original; 23 | } 24 | } -------------------------------------------------------------------------------- /WurstMod/Runtime/ObjectReferences.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reflection; 4 | using FistVR; 5 | using UnityEngine; 6 | using UnityEngine.SceneManagement; 7 | using WurstMod.MappingComponents.Generic; 8 | using WurstMod.Shared; 9 | using Object = UnityEngine.Object; 10 | 11 | // ReSharper disable UnassignedField.Global 12 | 13 | namespace WurstMod.Runtime 14 | { 15 | public static class ObjectReferences 16 | { 17 | #region Auto-set fields 18 | 19 | [ObjectReference] public static FVRPointableButton ButtonDonor; 20 | [ObjectReference] public static TNH_DestructibleBarrierPoint BarrierDonor; 21 | [ObjectReference] public static TNH_Manager ManagerDonor; 22 | [ObjectReference] public static TNH_HoldPoint HoldPointDonor; 23 | [ObjectReference] public static SosigTestingPanel1 GroundPanel; 24 | [ObjectReference] public static FVRReverbSystem ReverbSystem; 25 | [ObjectReference] public static FVRSceneSettings FVRSceneSettings; 26 | [ObjectReference] public static MainMenuScreen MainMenuControls; 27 | [ObjectReference] public static AIManager AIManager; 28 | 29 | // These get marked as Don't Destroy On Load because we kind of need them to exist after a reload :/ 30 | [ObjectReference("ItemSpawner", true, true)] public static GameObject ItemSpawnerDonor; 31 | [ObjectReference("Destructobin", true)] public static GameObject DestructobinDonor; 32 | [ObjectReference("SosigSpawner", true)] public static GameObject SosigSpawnerDonor; 33 | [ObjectReference("WhizzBangADinger2", true)] public static GameObject WhizzBangADingerDonor; 34 | [ObjectReference("BangerDetonator", true)] public static GameObject BangerDetonatorDonor; 35 | 36 | [ObjectReference("[CameraRig]Fixed")] public static GameObject CameraRig; 37 | [ObjectReference("[ResetPoint]")] public static GameObject ResetPoint; 38 | 39 | #endregion 40 | 41 | #region Manually-set fields 42 | 43 | /// 44 | /// This is set just after the custom scene is loading. It will always be set before custom code is ran. 45 | /// 46 | public static CustomScene CustomScene; 47 | 48 | #endregion 49 | 50 | 51 | /// 52 | /// Tries to find the appropriate object reference for the attached field. 53 | /// This should be called on scene load before anything messes with 54 | /// it to ensure the objects aren't destroyed. 55 | /// 56 | public static void FindReferences(Scene scene) 57 | { 58 | // A list of every GameObject in the scene 59 | var gameObjects = scene.GetAllGameObjectsInScene(); 60 | 61 | // First we find every field that uses this attribute 62 | foreach (var field in AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes()).SelectMany(x => x.GetFields())) 63 | { 64 | // Find our attributes on that field and continue if there aren't any 65 | var attributes = field.GetCustomAttributes(typeof(ObjectReferenceAttribute), true).Cast().ToArray(); 66 | if (attributes.Length == 0) continue; 67 | 68 | // Reset the field and go over each found attribute 69 | //field.SetValue(null, null); 70 | foreach (var reference in attributes) 71 | { 72 | // If the field's value is already set, break. 73 | // NOTE: Objects are "deleted" on scene load, but reflection will not necessarily return null immediately. 74 | // This method is only run on scene load anyway, so the duplicate checking isn't super necessary. 75 | if (field.GetValue(null) as Object) break; 76 | 77 | // If the field type is GameObject, just find the GameObject normally 78 | Object found; 79 | if (field.FieldType == typeof(GameObject)) 80 | found = gameObjects.FirstOrDefault(x => reference.MatchNameExactly ? x.name == reference.NameFilter : x.name.Contains(reference.NameFilter)); 81 | // If it isn't, we also want to query for where it has a component of the right type 82 | else 83 | found = gameObjects.Where(x => reference.MatchNameExactly ? x.name == reference.NameFilter : x.name.Contains(reference.NameFilter)).FirstOrDefault(x => x.GetComponent(field.FieldType))?.GetComponent(field.FieldType); 84 | 85 | if (found == null) continue; 86 | field.SetValue(null, found); 87 | 88 | if (!reference.DontDestroyOnLoad) continue; 89 | if (found is GameObject go) 90 | { 91 | go.transform.parent = null; 92 | go.transform.position = Vector3.down * 1000; 93 | } 94 | else 95 | { 96 | ((Component) found).transform.parent = null; 97 | ((Component) found).transform.position = Vector3.down * 1000; 98 | } 99 | Object.DontDestroyOnLoad(found); 100 | } 101 | } 102 | } 103 | } 104 | 105 | /// Attribute to represent a field that is auto-filled with an object reference when a scene is loaded. 106 | /// 107 | /// 108 | /// // Provides a reference to the scene's Camera Rig. 109 | /// [ObjectReference("[CameraRig]Fixed")] 110 | /// public static GameObject CameraRig; 111 | /// 112 | /// 113 | [AttributeUsage(AttributeTargets.Field)] 114 | public class ObjectReferenceAttribute : Attribute 115 | { 116 | public ObjectReferenceAttribute(string nameFilter = "", bool dontDestroyOnLoad = false, bool matchNameExactly = false) 117 | { 118 | NameFilter = nameFilter; 119 | DontDestroyOnLoad = dontDestroyOnLoad; 120 | MatchNameExactly = matchNameExactly; 121 | } 122 | 123 | public string NameFilter { get; } 124 | public bool DontDestroyOnLoad { get; } 125 | public bool MatchNameExactly { get; } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /WurstMod/Runtime/Patches.cs: -------------------------------------------------------------------------------- 1 | #if !UNITY_EDITOR 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Reflection; 6 | using FistVR; 7 | using HarmonyLib; 8 | using UnityEngine; 9 | using WurstMod.MappingComponents.Generic; 10 | using WurstMod.MappingComponents.TakeAndHold; 11 | using WurstMod.Shared; 12 | 13 | namespace WurstMod.Runtime 14 | { 15 | public class Patches 16 | { 17 | static Harmony harmony; 18 | 19 | public static void Patch() 20 | { 21 | harmony = new Harmony("com.koba.plugins.wurstmod"); 22 | 23 | // MUST patch GetTypes first. 24 | // This might result in a double-patch for GetTypes but it won't hurt anything. 25 | harmony.CreateClassProcessor(typeof(Patch_Assembly)).Patch(); 26 | harmony.PatchAll(Assembly.GetExecutingAssembly()); 27 | } 28 | } 29 | 30 | [HarmonyPatch(typeof(TNH_UIManager), nameof(TNH_UIManager.UpdateLevelSelectDisplayAndLoader))] 31 | class TNH_UIManager_UpdateLevelSelectDisplayAndLoader 32 | { 33 | static bool Prefix(TNH_UIManager __instance) 34 | { 35 | TNH_UIManager.LevelData level = __instance.GetLevelData(__instance.CurLevelID); 36 | if (level is ModdedLevelInfo moddedLevelInfo) 37 | Loader.LevelToLoad = moddedLevelInfo.Original; 38 | else Loader.LevelToLoad = null; 39 | return true; 40 | } 41 | } 42 | 43 | #region Assembly GetTypes Hacking 44 | // Assembly.GetTypes() fails if ANY of the types cannot be loaded. 45 | // In practice, this means we cannot inherit from UnityEditor. 46 | // To fix this, we will literally patch mscorlib.dll. 47 | [HarmonyPatch(typeof(Assembly), "GetTypes", new Type[0])] 48 | public class Patch_Assembly 49 | { 50 | static Exception Finalizer(Exception __exception, ref Type[] __result) 51 | { 52 | if (__exception != null) 53 | { 54 | __result = ((ReflectionTypeLoadException)__exception).Types.Where(t => t != null).ToArray(); 55 | } 56 | return null; 57 | } 58 | } 59 | 60 | #endregion 61 | 62 | #region Scoreboard Disabling 63 | 64 | /// 65 | /// This patch prevents high scores from being submitted online when playing 66 | /// a custom level, preserving the legitimacy of the leaderboards. 67 | /// 68 | [HarmonyPatch(typeof(TNH_ScoreDisplay), "ProcessHighScore")] 69 | public class Patch_TNH_ScoreDisplay 70 | { 71 | static bool Prefix(int score) 72 | { 73 | if (Loader.LevelToLoad != null) 74 | { 75 | // Returning false on a prefix prevents the original method from running. 76 | Debug.Log("Ignoring high score for custom level."); 77 | return false; 78 | } 79 | 80 | return true; 81 | } 82 | } 83 | 84 | #endregion 85 | 86 | #region Auto-Generated Off-Mesh Link Support 87 | 88 | /// 89 | /// Sosig's don't actually support automatically generated off-mesh links. 90 | /// This patch prevents some console spam and maybe a crash possibility? 91 | /// 92 | [HarmonyPatch(typeof(Sosig), "LegsUpdate_MoveToPoint")] 93 | public class Patch_LegsUpdate_MoveToPoint 94 | { 95 | // Hold onto Sosigs and their local NavMeshLinkExtension. 96 | static Dictionary genData = new Dictionary(); 97 | 98 | static bool Prefix(Sosig __instance) 99 | { 100 | if (__instance.Agent.isOnOffMeshLink && __instance.Agent.currentOffMeshLinkData.offMeshLink == null) 101 | { 102 | if (!__instance.ReflectGet("m_isOnOffMeshLink")) 103 | { 104 | // Setup fake link, which is added as a component to the Sosig. 105 | NavMeshLinkExtension fakeLink; 106 | if (genData.ContainsKey(__instance)) 107 | { 108 | fakeLink = genData[__instance]; 109 | } 110 | else 111 | { 112 | fakeLink = __instance.gameObject.AddComponent(); 113 | genData[__instance] = fakeLink; 114 | } 115 | 116 | 117 | // Force fields on the fake link to match the type of jump. 118 | Vector3 jump = __instance.Agent.currentOffMeshLinkData.endPos - __instance.Agent.currentOffMeshLinkData.startPos; 119 | float horzMag = new Vector3(jump.x, 0, jump.z).magnitude; 120 | float vertMag = Mathf.Abs(jump.y); 121 | bool jumpDown = jump.y < 0; 122 | 123 | if (!jumpDown && vertMag > (2 * horzMag)) fakeLink.Type = NavMeshLinkExtension.NavMeshLinkType.Ladder; 124 | else if (jumpDown && vertMag > (2 * horzMag)) fakeLink.Type = NavMeshLinkExtension.NavMeshLinkType.Drop; 125 | else fakeLink.Type = NavMeshLinkExtension.NavMeshLinkType.LateralJump; 126 | 127 | // Arbitrary number. 128 | fakeLink.ReflectSet("m_xySpeed", 0.5f); 129 | 130 | 131 | // Clean up the dictionary. 132 | // Destroyed Unity objects sure have some confusing properties. 133 | List nullCheck = new List(); 134 | foreach (var pair in genData) 135 | { 136 | if (pair.Key == null) nullCheck.Add(pair.Key); 137 | } 138 | 139 | foreach (Sosig destroyed in nullCheck) 140 | { 141 | genData.Remove(destroyed); 142 | } 143 | 144 | 145 | __instance.ReflectSet("m_isOnOffMeshLink", true); 146 | __instance.ReflectInvoke("InitiateLink", fakeLink); 147 | } 148 | } 149 | 150 | return true; 151 | } 152 | } 153 | 154 | /// 155 | /// The Start function of NavMeshLinkExtension also relies on existence of an OffMeshLink. 156 | /// So we create our own second case. Might be hard to avoid GetComponent here, but ah well. 157 | /// 158 | [HarmonyPatch(typeof(NavMeshLinkExtension), "Start")] 159 | public class Patch_NavMeshLinkExtension_Start 160 | { 161 | static bool Prefix(NavMeshLinkExtension __instance) 162 | { 163 | // Skip the Start method if this gameObject is a Sosig. 164 | return __instance.gameObject.GetComponent() == null; 165 | } 166 | } 167 | 168 | #endregion 169 | 170 | #region ForcedSpawn SpawnOnly Support 171 | 172 | /// 173 | /// To enforce a supply point as SpawnOnly, we need to remove it after its used 174 | /// This patch lets us do that safely. 175 | /// 176 | [HarmonyPatch(typeof(TNH_Manager), "InitBeginningEquipment")] 177 | public class Patch_TNH_Manager_InitBeginningEquipment 178 | { 179 | static void Postfix(TNH_Manager __instance) 180 | { 181 | var forcedSpawn = __instance.SupplyPoints.Select(x => x.GetComponent()).FirstOrDefault(x => x != null); 182 | if (forcedSpawn == null) return; 183 | __instance.SupplyPoints = __instance.SupplyPoints.Where(x => x.gameObject != forcedSpawn.gameObject).ToList(); 184 | __instance.ReflectGet("m_curPointSequence").StartSupplyPointIndex = 0; 185 | } 186 | } 187 | 188 | #endregion 189 | 190 | #region Generic Level Support 191 | 192 | [HarmonyPatch(typeof(MainMenuScenePointable), "OnPoint")] 193 | public class Patch_MainMenuScenePointable_OnPoint 194 | { 195 | static bool Prefix(MainMenuScenePointable __instance, FVRViveHand hand) 196 | { 197 | if (hand.Input.TriggerDown) 198 | { 199 | if (__instance.name == "MODDEDSCREEN") 200 | { 201 | var levelId = __instance.Def.Name.Split('\n')[1]; 202 | var level = CustomLevelFinder.EnumerateLevelInfos().FirstOrDefault(x => x.Identifier == levelId); 203 | Loader.LevelToLoad = level; 204 | } 205 | else Loader.LevelToLoad = null; 206 | } 207 | 208 | 209 | return true; 210 | } 211 | } 212 | 213 | #endregion 214 | 215 | #region Target Event Support 216 | 217 | [HarmonyPatch(typeof(ReactiveSteelTarget), "Damage")] 218 | public class Patch_ReactiveSteelTarget_Damage 219 | { 220 | static Dictionary targetComponents = new Dictionary(); 221 | 222 | static void Postfix(ReactiveSteelTarget __instance, FistVR.Damage dam) 223 | { 224 | // Original method ignores non projectile damage 225 | if (dam.Class != Damage.DamageClass.Projectile) return; 226 | 227 | __instance.SendMessage("TargetHit"); 228 | 229 | // Cache Target components. 230 | Target ourTarget = null; 231 | if (targetComponents.ContainsKey(__instance)) 232 | { 233 | ourTarget = targetComponents[__instance]; 234 | } 235 | else 236 | { 237 | ourTarget = __instance.GetComponent(); 238 | targetComponents[__instance] = ourTarget; 239 | } 240 | 241 | // Clear cache as necessary, using weird Unity destroyed behaviour. 242 | List nullCheck = new List(); 243 | foreach (var pair in targetComponents) 244 | { 245 | if (pair.Key == null) nullCheck.Add(pair.Key); 246 | } 247 | 248 | foreach (ReactiveSteelTarget destroyed in nullCheck) 249 | { 250 | targetComponents.Remove(destroyed); 251 | } 252 | 253 | // Run event logic. 254 | if (ourTarget != null) 255 | { 256 | if (ourTarget.shotEvent != null) ourTarget.shotEvent.Invoke(); 257 | } 258 | } 259 | } 260 | 261 | #endregion 262 | 263 | #region Take and Hold Spawn Softlock Fixer 264 | 265 | /// 266 | /// All of the Spawn patches do the same thing: Stop trying to spawn enemies if 267 | /// we run out of spawnpoints! This is most easily accomplished with a finalizer. 268 | /// Note this may truncate spawns severely for SpawnHoldEnemyGroup, but that's 269 | /// much better than softlocking entirely, and it only happens in "improperly" set up holds. 270 | /// 271 | [HarmonyPatch(typeof(FistVR.TNH_SupplyPoint), "SpawnTakeEnemyGroup")] 272 | public class Patch_TNH_SupplyPoint_SpawnTakeEnemyGroup 273 | { 274 | static Exception Finalizer(Exception __exception) 275 | { 276 | if (__exception is ArgumentOutOfRangeException) 277 | { 278 | Debug.Log("DEBUG: Hit Spawn finalizer"); 279 | return null; // Ignore exception. 280 | } 281 | return __exception; 282 | } 283 | } 284 | 285 | [HarmonyPatch(typeof(FistVR.TNH_HoldPoint), "SpawnTakeEnemyGroup")] 286 | public class Patch_TNH_HoldPoint_SpawnTakeEnemyGroup 287 | { 288 | static Exception Finalizer(Exception __exception) 289 | { 290 | if (__exception is ArgumentOutOfRangeException) 291 | { 292 | Debug.Log("DEBUG: Hit Spawn finalizer"); 293 | return null; // Ignore exception. 294 | } 295 | return __exception; 296 | } 297 | } 298 | 299 | [HarmonyPatch(typeof(FistVR.TNH_HoldPoint), "SpawnHoldEnemyGroup")] 300 | public class Patch_TNH_HoldPoint_SpawnHoldEnemyGroup 301 | { 302 | static Exception Finalizer(Exception __exception, FistVR.TNH_HoldPoint __instance) 303 | { 304 | if (__exception is ArgumentOutOfRangeException) 305 | { 306 | Debug.Log("DEBUG: Hit Spawn finalizer"); 307 | __instance.ReflectSet("m_isFirstWave", false); 308 | return null; // Ignore exception. 309 | } 310 | return __exception; 311 | } 312 | } 313 | 314 | #endregion 315 | } 316 | #endif 317 | -------------------------------------------------------------------------------- /WurstMod/Runtime/SceneLoaders/CustomSceneLoader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using UnityEngine; 5 | using WurstMod.MappingComponents.Generic; 6 | using WurstMod.Shared; 7 | 8 | namespace WurstMod.Runtime 9 | { 10 | /// 11 | /// Abstract class to derive custom scene loaders for 12 | /// 13 | public abstract class CustomSceneLoader 14 | { 15 | /// 16 | /// This is implemented by the deriving class and is a unique identifier for a game mode. 17 | /// 18 | public abstract string GamemodeId { get; } 19 | 20 | /// 21 | /// This is implemented by the deriving class and tells the loader which scene to use as the base for the game mode 22 | /// 23 | public abstract string BaseScene { get; } 24 | 25 | /// 26 | /// This list contains the names of all objects that should be removed from the scene when loading a new level 27 | /// 28 | public virtual IEnumerable EnumerateDestroyOnLoad() => new[] {"!ftraceLightmaps"}.AsEnumerable(); 29 | 30 | public CustomScene LevelRoot { get; set; } 31 | 32 | /// 33 | /// This method is called by the loader class before it has done anything. The original scene will be intact 34 | /// and unmodified 35 | /// 36 | public virtual void PreLoad() 37 | { 38 | } 39 | 40 | /// 41 | /// This method is called by the loader class after it has finished loading the modded scene. 42 | /// All proxies will have been resolved and the original scene will have been stripped of any 43 | /// unused game objects. 44 | /// 45 | public virtual void PostLoad() 46 | { 47 | } 48 | 49 | /// 50 | /// Called when the loader is finished resolving components but just before everything is re-activated. 51 | /// 52 | public virtual void Resolve() 53 | { 54 | if (ObjectReferences.AIManager) 55 | { 56 | ObjectReferences.AIManager.NumEntitiesToCheckPerFrame = 57 | ObjectReferences.CustomScene.NumEntitiesToCheckPerFrame; 58 | } 59 | } 60 | 61 | public static CustomSceneLoader GetSceneLoaderForGamemode(string gamemode) 62 | { 63 | // Get a list of all types in the app domain that derive from CustomSceneLoader 64 | var types = AppDomain.CurrentDomain.GetAssemblies() 65 | .SelectMany(a => a.GetTypes()) 66 | .Where(t => t.IsSubclassOf(typeof(CustomSceneLoader))); 67 | 68 | // Magic LINQ statement to select the first type that has the 69 | // gamemode that matches the gamemode parameter 70 | return types 71 | .Where(x => x.IsSubclassOf(typeof(CustomSceneLoader))) 72 | .Select(x => Activator.CreateInstance(x) as CustomSceneLoader) 73 | .FirstOrDefault(x => x != null && x.GamemodeId == gamemode); 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /WurstMod/Runtime/SceneLoaders/SandboxSceneLoader.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using WurstMod.Runtime; 4 | using WurstMod.Shared; 5 | 6 | namespace WurstMod.SceneLoaders 7 | { 8 | public class SandboxSceneLoader : CustomSceneLoader 9 | { 10 | public override string GamemodeId => Constants.GamemodeSandbox; 11 | public override string BaseScene => "ProvingGround"; 12 | 13 | public override IEnumerable EnumerateDestroyOnLoad() 14 | { 15 | 16 | return base.EnumerateDestroyOnLoad().Concat(new[] 17 | { 18 | "_Animator_Spawning_", 19 | "_Boards", 20 | "_Env", 21 | "_AmbientAudio", 22 | "AILadderTest1", 23 | // TODO: Should probably remove all the Anvil Prefabs, but it causes errors... 24 | //"__SpawnOnLoad", 25 | }); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /WurstMod/Runtime/SceneLoaders/TakeAndHoldSceneLoader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using FistVR; 5 | using UnityEngine; 6 | using WurstMod.MappingComponents.TakeAndHold; 7 | using WurstMod.Shared; 8 | using Random = UnityEngine.Random; 9 | using TNH_HoldPoint = FistVR.TNH_HoldPoint; 10 | using TNH_SupplyPoint = FistVR.TNH_SupplyPoint; 11 | 12 | namespace WurstMod.Runtime.SceneLoaders 13 | { 14 | public class TakeAndHoldSceneLoader : CustomSceneLoader 15 | { 16 | private TNH_Manager _tnhManager; 17 | private string LevelIdentifier; 18 | 19 | public override string GamemodeId => Constants.GamemodeTakeAndHold; 20 | public override string BaseScene => "TakeAndHoldClassic"; 21 | 22 | public override IEnumerable EnumerateDestroyOnLoad() 23 | { 24 | return base.EnumerateDestroyOnLoad().Concat(new[] 25 | { 26 | // Take and Hold objects 27 | "HoldPoint_", 28 | "Ladders", 29 | "Lighting", 30 | "OpenArea", 31 | "RampHelperCubes", 32 | "ReflectionProbes", 33 | "SupplyPoint_", 34 | "Tiles" 35 | }); 36 | } 37 | 38 | /// 39 | /// Base function for setting up the TNH Manager object to handle a custom level. 40 | /// 41 | public override void Resolve() 42 | { 43 | #if !UNITY_EDITOR 44 | _tnhManager = ObjectReferences.ManagerDonor; 45 | LevelIdentifier = Loader.LevelToLoad?.Identifier ?? 46 | throw new Exception("Invalid state. LevelToLoad was null in TNHSceneLoader Resolve"); 47 | _tnhManager.LevelName = LevelIdentifier; 48 | 49 | // Hold points need to be set. 50 | _tnhManager.HoldPoints = LevelRoot.GetComponentsInChildren(true).ToList(); 51 | 52 | // Supply points need to be set. 53 | _tnhManager.SupplyPoints = LevelRoot.GetComponentsInChildren(true).ToList(); 54 | 55 | // Possible Sequences need to be generated at random. 56 | if (LevelRoot.ExtraData == null || LevelRoot.ExtraData[0].Value == "") _tnhManager.PossibleSequnces = GenerateRandomPointSequences(10); 57 | 58 | // Safe Pos Matrix needs to be set. Diagonal for now. 59 | TNH_SafePositionMatrix maxMatrix = GenerateTestMatrix(); 60 | _tnhManager.SafePosMatrix = maxMatrix; 61 | #endif 62 | } 63 | 64 | /// 65 | /// Regular gamemode uses a preset list of possible hold orders. This creates a bunch randomly. 66 | /// 67 | private List GenerateRandomPointSequences(int count) 68 | { 69 | // Temporarily initialize the random seed to match the level's identifier 70 | Random.State oldState = Random.state; 71 | Random.InitState(LevelIdentifier.GetHashCode()); 72 | List sequences = new List(); 73 | for (int ii = 0; ii < count; ii++) 74 | { 75 | TNH_PointSequence sequence = ScriptableObject.CreateInstance(); 76 | 77 | // Logic for forced spawn location. 78 | ForcedSpawn forcedSpawn = LevelRoot.GetComponentInChildren(); 79 | if (forcedSpawn != null) 80 | { 81 | sequence.StartSupplyPointIndex = _tnhManager.SupplyPoints.IndexOf(_tnhManager.SupplyPoints.First(x => x.gameObject == forcedSpawn.gameObject)); 82 | } 83 | else 84 | { 85 | sequence.StartSupplyPointIndex = Random.Range(0, _tnhManager.SupplyPoints.Count); 86 | } 87 | 88 | // If the mapper hasn't set a custom hold order 89 | if (LevelRoot.ExtraData == null || LevelRoot.ExtraData[0].Value == "") 90 | sequence.HoldPoints = new List 91 | { 92 | Random.Range(0, _tnhManager.HoldPoints.Count), 93 | Random.Range(0, _tnhManager.HoldPoints.Count), 94 | Random.Range(0, _tnhManager.HoldPoints.Count), 95 | Random.Range(0, _tnhManager.HoldPoints.Count), 96 | Random.Range(0, _tnhManager.HoldPoints.Count) 97 | }; 98 | else 99 | { 100 | var namedOrder = LevelRoot.ExtraData[0].Value.Split(','); 101 | sequence.HoldPoints = namedOrder.Select(x => _tnhManager.HoldPoints.FindIndex(y => y.name == x)).ToList(); 102 | } 103 | 104 | // Fix sequence, because they may generate the same point after the current point, IE {1,4,4) 105 | // This would break things. 106 | for (int jj = 0; jj < sequence.HoldPoints.Count - 1; jj++) 107 | { 108 | if (sequence.HoldPoints[jj] == sequence.HoldPoints[jj + 1]) 109 | { 110 | sequence.HoldPoints[jj + 1] = (sequence.HoldPoints[jj + 1] + 1) % _tnhManager.HoldPoints.Count; 111 | } 112 | } 113 | 114 | sequences.Add(sequence); 115 | } 116 | 117 | // Set the random state back to what it was before 118 | Random.state = oldState; 119 | 120 | return sequences; 121 | } 122 | 123 | /// 124 | /// Creates a matrix of valid Hold Points and Supply Points (I think) only used for endless? 125 | /// By default, just generates a matrix that is false on diagonals. 126 | /// 127 | private TNH_SafePositionMatrix GenerateTestMatrix() 128 | { 129 | TNH_SafePositionMatrix maxMatrix = ScriptableObject.CreateInstance(); 130 | maxMatrix.Entries_HoldPoints = new List(); 131 | maxMatrix.Entries_SupplyPoints = new List(); 132 | 133 | int effectiveHoldCount = _tnhManager.HoldPoints.Count; 134 | int effectiveSupplyCount = _tnhManager.SupplyPoints.Where(x => x.GetComponent() == null || x.GetComponent().spawnOnly == false).Count(); 135 | //int effectiveSupplyCount = _tnhManager.SupplyPoints.Count; 136 | 137 | for (int ii = 0; ii < effectiveHoldCount; ii++) 138 | { 139 | TNH_SafePositionMatrix.PositionEntry entry = new TNH_SafePositionMatrix.PositionEntry(); 140 | entry.SafePositions_HoldPoints = new List(); 141 | for (int jj = 0; jj < effectiveHoldCount; jj++) 142 | { 143 | entry.SafePositions_HoldPoints.Add(ii != jj); 144 | } 145 | 146 | entry.SafePositions_SupplyPoints = new List(); 147 | for (int jj = 0; jj < effectiveSupplyCount; jj++) 148 | { 149 | entry.SafePositions_SupplyPoints.Add(true); 150 | } 151 | 152 | maxMatrix.Entries_HoldPoints.Add(entry); 153 | } 154 | 155 | for (int ii = 0; ii < effectiveSupplyCount; ii++) 156 | { 157 | TNH_SafePositionMatrix.PositionEntry entry = new TNH_SafePositionMatrix.PositionEntry(); 158 | entry.SafePositions_HoldPoints = new List(); 159 | for (int jj = 0; jj < effectiveHoldCount; jj++) 160 | { 161 | entry.SafePositions_HoldPoints.Add(true); 162 | } 163 | 164 | entry.SafePositions_SupplyPoints = new List(); 165 | for (int jj = 0; jj < effectiveSupplyCount; jj++) 166 | { 167 | entry.SafePositions_SupplyPoints.Add(ii != jj); 168 | } 169 | 170 | maxMatrix.Entries_SupplyPoints.Add(entry); 171 | } 172 | 173 | return maxMatrix; 174 | } 175 | } 176 | } -------------------------------------------------------------------------------- /WurstMod/Runtime/ScenePatchers/Generic_LevelPopulator.cs: -------------------------------------------------------------------------------- 1 | #if !UNITY_EDITOR 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using FistVR; 6 | using UnityEngine; 7 | using UnityEngine.SceneManagement; 8 | using UnityEngine.UI; 9 | using WurstMod.Shared; 10 | 11 | namespace WurstMod.Runtime.ScenePatchers 12 | { 13 | class Generic_LevelPopulator 14 | { 15 | // References 16 | private static GameObject labelBase; 17 | private static GameObject sceneScreenBase; 18 | private static GameObject changelogPanel; 19 | 20 | // Etc 21 | private static List screenPositions = new List(); 22 | private static List screens = new List(); 23 | private static List levelPaths = new List(); 24 | 25 | public static void SetupLevelPopulator(Scene loaded) 26 | { 27 | if (loaded.name == "MainMenu3") 28 | { 29 | Debug.Log("Initializing level populator..."); 30 | Reset(); 31 | GatherReferences(); 32 | CalculateScreenPositions(); 33 | InitObjects(); 34 | SetupLevelDefs(); 35 | //SetupPanel(); TODO make this useful. 36 | } 37 | } 38 | 39 | private static void Reset() 40 | { 41 | Loader.LevelToLoad = null; 42 | screenPositions.Clear(); 43 | screens.Clear(); 44 | levelPaths.Clear(); 45 | } 46 | 47 | private static void GatherReferences() 48 | { 49 | sceneScreenBase = GameObject.Find("SceneScreen_GDC"); 50 | labelBase = GameObject.Find("Label_Description_1_Title (5)"); 51 | changelogPanel = GameObject.Find("MainScreen1"); 52 | } 53 | 54 | private static void CalculateScreenPositions() 55 | { 56 | // Get a circle. 57 | Func CircleX = x => (14.13f * Mathf.Cos(Mathf.Deg2Rad * x)) - 0.39f; 58 | Func CircleZ = z => (14.13f * Mathf.Sin(Mathf.Deg2Rad * z)) - 2.98f; 59 | for (int ii = 0; ii <= 360; ii += 13) 60 | { 61 | for (int jj = 0; jj < 4; jj++) 62 | { 63 | screenPositions.Add(new Vector3(CircleX(ii), 0.5f + (jj * 2), CircleZ(ii))); 64 | } 65 | } 66 | 67 | // Trimming positions we don't want and order by -z. 68 | screenPositions = screenPositions.Where(x => x.z < -7f).ToList(); 69 | screenPositions = screenPositions.OrderByDescending(x => -x.z).ThenBy(x => Mathf.Abs(x.y - 4.15f)).ToList(); 70 | 71 | // Compatibility with ATLAS. Only use half the screen positions. Atlas will use the other half. 72 | screenPositions = screenPositions.Where(x => x.x < 0).ToList(); 73 | } 74 | 75 | private static void InitObjects() 76 | { 77 | // Modded Levels label. 78 | GameObject moddedScenesLabel = GameObject.Instantiate(labelBase, labelBase.transform.parent); 79 | moddedScenesLabel.transform.position = new Vector3(-4f, 8.3f, -15.5f); 80 | moddedScenesLabel.transform.localEulerAngles = new Vector3(-180f, 30f, 180f); 81 | moddedScenesLabel.GetComponent().text = "WurstMod Sandbox Scenes"; 82 | 83 | // Scene screens. 84 | for (int ii = 0; ii < screenPositions.Count; ii++) 85 | { 86 | // Create and position properly. Rename so patch can handle it properly. 87 | GameObject screen = GameObject.Instantiate(sceneScreenBase, sceneScreenBase.transform.parent); 88 | screen.transform.position = screenPositions[ii]; 89 | //screen.transform.LookAt(Vector3.zero, Vector3.up); 90 | screen.transform.localEulerAngles = new Vector3(0, 180 - (Mathf.Rad2Deg * Mathf.Atan(-screen.transform.position.x / screen.transform.position.z)), 0); 91 | screen.transform.localScale = 0.5f * screen.transform.localScale; 92 | screen.name = "MODDEDSCREEN"; 93 | 94 | // Make sure scaling is set correctly. 95 | MainMenuScenePointable screenComponent = screen.GetComponent(); 96 | screenComponent.ReflectSet("m_startScale", screen.transform.localScale); 97 | 98 | // Add to list and disable until needed. 99 | screens.Add(screenComponent); 100 | screen.SetActive(false); 101 | } 102 | } 103 | 104 | private static void SetupLevelDefs() 105 | { 106 | var levels = CustomLevelFinder.EnumerateLevelInfos().Where(x => x.Gamemode != Constants.GamemodeTakeAndHold).ToArray(); 107 | for (int ii = 0; ii < levels.Length; ii++) 108 | { 109 | var level = levels[ii]; 110 | 111 | var imageT = level.Thumbnail; 112 | Texture2D image = null; 113 | if (imageT) image = imageT; 114 | 115 | // Create and apply scene def. 116 | MainMenuSceneDef moddedDef = ScriptableObject.CreateInstance(); 117 | moddedDef.Name = level.SceneName + "\n" + level.Identifier; 118 | moddedDef.Type = level.Author; 119 | moddedDef.SceneName = "ProvingGround"; 120 | moddedDef.Desciption = level.Description; 121 | moddedDef.Image = level.Sprite; 122 | 123 | MainMenuScenePointable screen = screens.First(x => !x.gameObject.activeSelf); 124 | screen.Def = moddedDef; 125 | screen.GetComponent().material.SetTexture("_MainTex", imageT); 126 | 127 | // Enable the screen now that it has been set up. 128 | screen.gameObject.SetActive(true); 129 | } 130 | } 131 | 132 | private static void SetupPanel() 133 | { 134 | // Copy the changelog panel for my own purposes. 135 | GameObject panel = GameObject.Instantiate(changelogPanel, changelogPanel.transform.parent); 136 | panel.transform.position = new Vector3(-1.805f, 2.15f, -7.561f); 137 | panel.transform.localEulerAngles = new Vector3(0f, 13.91f, 0f); 138 | panel.name = "MODPANEL"; 139 | 140 | // Two text fields, title and body. 141 | Text[] texts = panel.GetComponentsInChildren(); 142 | Text title = texts[0]; 143 | Text body = texts[1]; 144 | 145 | //TODO Read from web source. 146 | title.text = "Welcome to WurstMod!"; 147 | body.text = "This panel will be used to provide information and updates about WurstMod. An UPDATE button will appear below if there is an update released."; 148 | 149 | //TODO Update button. 150 | } 151 | } 152 | } 153 | #endif -------------------------------------------------------------------------------- /WurstMod/Runtime/ScenePatchers/TNH_LevelSelector.cs: -------------------------------------------------------------------------------- 1 | #if !UNITY_EDITOR 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using FistVR; 6 | using UnityEngine; 7 | using UnityEngine.SceneManagement; 8 | using UnityEngine.UI; 9 | using WurstMod.Shared; 10 | 11 | namespace WurstMod.Runtime.ScenePatchers 12 | { 13 | /// 14 | /// This class is responsible for modifying the level selector panel to actually function. 15 | /// 16 | public static class TNH_LevelSelector 17 | { 18 | /// 19 | /// Performs all actions required to setup the level selector in the TNH Lobby. 20 | /// 21 | public static void SetupLevelSelector(Scene loaded) 22 | { 23 | if (loaded.name == "TakeAndHold_Lobby_2") 24 | { 25 | var uiManager = Object.FindObjectOfType(); 26 | foreach (LevelInfo level in CustomLevelFinder.ArchiveLevels.Where(x => x.Gamemode == Constants.GamemodeTakeAndHold)) 27 | uiManager.Levels.Add(new ModdedLevelInfo(level)); 28 | } 29 | } 30 | } 31 | } 32 | #endif -------------------------------------------------------------------------------- /WurstMod/Runtime/SosigSpawnerHelper.cs: -------------------------------------------------------------------------------- 1 | #if !UNITY_EDITOR 2 | using System; 3 | using System.Collections.Generic; 4 | using FistVR; 5 | using UnityEngine; 6 | using Random = UnityEngine.Random; 7 | using Object = UnityEngine.Object; 8 | 9 | namespace WurstMod.Runtime 10 | { 11 | public static class SosigSpawnerHelper 12 | { 13 | public static void SpawnSosigWithTemplate(SosigEnemyTemplate template, SpawnOptions spawnOptions, Vector3 position, 14 | Vector3 forward) 15 | { 16 | var sosigPrefab = template.SosigPrefabs[Random.Range(0, template.SosigPrefabs.Count)]; 17 | var configTemplate = template.ConfigTemplates[Random.Range(0, template.ConfigTemplates.Count)]; 18 | var outfitConfig = template.OutfitConfig[Random.Range(0, template.OutfitConfig.Count)]; 19 | var sosig = SpawnSosigAndConfigureSosig(sosigPrefab.GetGameObject(), position, 20 | Quaternion.LookRotation(forward, Vector3.up), configTemplate, outfitConfig); 21 | sosig.InitHands(); 22 | sosig.Inventory.Init(); 23 | if (template.WeaponOptions.Count > 0) 24 | { 25 | var weapon = SpawnWeapon(template.WeaponOptions); 26 | weapon.SetAutoDestroy(true); 27 | sosig.ForceEquip(weapon); 28 | if (weapon.Type == SosigWeapon.SosigWeaponType.Gun && spawnOptions.SpawnWithFullAmmo) 29 | sosig.Inventory.FillAmmoWithType(weapon.AmmoType); 30 | } 31 | 32 | var spawnWithSecondaryWeapon = spawnOptions.EquipmentMode == 0 || spawnOptions.EquipmentMode == 2 || 33 | spawnOptions.EquipmentMode == 3 && 34 | Random.Range(0.0f, 1f) >= template.SecondaryChance; 35 | if (template.WeaponOptions_Secondary.Count > 0 && spawnWithSecondaryWeapon) 36 | { 37 | var weapon = SpawnWeapon(template.WeaponOptions_Secondary); 38 | weapon.SetAutoDestroy(true); 39 | sosig.ForceEquip(weapon); 40 | if (weapon.Type == SosigWeapon.SosigWeaponType.Gun && spawnOptions.SpawnWithFullAmmo) 41 | sosig.Inventory.FillAmmoWithType(weapon.AmmoType); 42 | } 43 | 44 | var spawnWithTertiaryWeapon = spawnOptions.EquipmentMode == 0 || 45 | spawnOptions.EquipmentMode == 3 && 46 | Random.Range(0.0f, 1f) >= template.TertiaryChance; 47 | if (template.WeaponOptions_Tertiary.Count > 0 && spawnWithTertiaryWeapon) 48 | { 49 | var w2 = SpawnWeapon(template.WeaponOptions_Tertiary); 50 | w2.SetAutoDestroy(true); 51 | sosig.ForceEquip(w2); 52 | if (w2.Type == SosigWeapon.SosigWeaponType.Gun && spawnOptions.SpawnWithFullAmmo) 53 | sosig.Inventory.FillAmmoWithType(w2.AmmoType); 54 | } 55 | 56 | var sosigIFF = spawnOptions.IFF; 57 | if (sosigIFF >= 5) 58 | sosigIFF = Random.Range(6, 10000); 59 | sosig.E.IFFCode = sosigIFF; 60 | sosig.CurrentOrder = Sosig.SosigOrder.Disabled; 61 | switch (spawnOptions.SpawnState) 62 | { 63 | case 0: 64 | sosig.FallbackOrder = Sosig.SosigOrder.Disabled; 65 | break; 66 | case 1: 67 | sosig.FallbackOrder = Sosig.SosigOrder.GuardPoint; 68 | break; 69 | case 2: 70 | sosig.FallbackOrder = Sosig.SosigOrder.Wander; 71 | break; 72 | case 3: 73 | sosig.FallbackOrder = Sosig.SosigOrder.Assault; 74 | break; 75 | } 76 | 77 | var targetPos = spawnOptions.SosigTargetPosition; 78 | var targetRot = spawnOptions.SosigTargetRotation; 79 | sosig.UpdateGuardPoint(targetPos); 80 | sosig.SetDominantGuardDirection(targetRot); 81 | sosig.UpdateAssaultPoint(targetPos); 82 | if (!spawnOptions.SpawnActivated) 83 | return; 84 | sosig.SetCurrentOrder(sosig.FallbackOrder); 85 | } 86 | 87 | private static SosigWeapon SpawnWeapon(List o) => Object 88 | .Instantiate(o[Random.Range(0, o.Count)].GetGameObject(), new Vector3(0.0f, 30f, 0.0f), Quaternion.identity) 89 | .GetComponent(); 90 | 91 | private static Sosig SpawnSosigAndConfigureSosig(GameObject prefab, Vector3 pos, Quaternion rot, SosigConfigTemplate t, 92 | SosigOutfitConfig w) 93 | { 94 | var componentInChildren = Object.Instantiate(prefab, pos, rot).GetComponentInChildren(); 95 | if (Random.Range(0.0f, 1f) < w.Chance_Headwear) 96 | SpawnAccessoryToLink(w.Headwear, componentInChildren.Links[0]); 97 | if (Random.Range(0.0f, 1f) < w.Chance_Facewear) 98 | SpawnAccessoryToLink(w.Facewear, componentInChildren.Links[0]); 99 | if (Random.Range(0.0f, 1f) < w.Chance_Eyewear) 100 | SpawnAccessoryToLink(w.Eyewear, componentInChildren.Links[0]); 101 | if (Random.Range(0.0f, 1f) < w.Chance_Torsowear) 102 | SpawnAccessoryToLink(w.Torsowear, componentInChildren.Links[1]); 103 | if (Random.Range(0.0f, 1f) < w.Chance_Pantswear) 104 | SpawnAccessoryToLink(w.Pantswear, componentInChildren.Links[2]); 105 | if (Random.Range(0.0f, 1f) < w.Chance_Pantswear_Lower) 106 | SpawnAccessoryToLink(w.Pantswear_Lower, componentInChildren.Links[3]); 107 | if (Random.Range(0.0f, 1f) < w.Chance_Backpacks) 108 | SpawnAccessoryToLink(w.Backpacks, componentInChildren.Links[1]); 109 | if (t.UsesLinkSpawns) 110 | { 111 | for (var index = 0; index < componentInChildren.Links.Count; ++index) 112 | { 113 | if (Random.Range(0.0f, 1f) < t.LinkSpawnChance[index]) 114 | componentInChildren.Links[index].RegisterSpawnOnDestroy(t.LinkSpawns[index]); 115 | } 116 | } 117 | 118 | componentInChildren.Configure(t); 119 | return componentInChildren; 120 | } 121 | 122 | private static void SpawnAccessoryToLink(List gs, SosigLink l) 123 | { 124 | if (gs.Count < 1) return; 125 | var accessory = Object.Instantiate(gs[Random.Range(0, gs.Count)].GetGameObject(), l.transform.position, 126 | l.transform.rotation); 127 | accessory.transform.SetParent(l.transform); 128 | accessory.GetComponent().RegisterWearable(l); 129 | } 130 | 131 | public struct SpawnOptions 132 | { 133 | public bool SpawnActivated; 134 | public int IFF; 135 | public bool SpawnWithFullAmmo; 136 | public int EquipmentMode; 137 | public int SpawnState; 138 | public Vector3 SosigTargetPosition; 139 | public Vector3 SosigTargetRotation; 140 | } 141 | } 142 | } 143 | #endif -------------------------------------------------------------------------------- /WurstMod/Runtime/SpriteLoader.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using UnityEngine; 3 | 4 | namespace WurstMod.Runtime 5 | { 6 | // Thanks to Freznosis from Unity Forums https://forum.unity.com/threads/generating-sprites-dynamically-from-png-or-jpeg-files-in-c.343735/ 7 | public static class SpriteLoader 8 | { 9 | public static Sprite LoadNewSprite(string FilePath, float PixelsPerUnit = 100.0f, SpriteMeshType spriteType = SpriteMeshType.Tight) 10 | { 11 | Texture2D SpriteTexture = LoadTexture(FilePath); 12 | Sprite NewSprite = Sprite.Create(SpriteTexture, new Rect(0, 0, SpriteTexture.width, SpriteTexture.height), new Vector2(0, 0), PixelsPerUnit, 0, spriteType); 13 | 14 | return NewSprite; 15 | } 16 | 17 | public static Sprite ConvertTextureToSprite(Texture2D texture, float PixelsPerUnit = 100.0f, SpriteMeshType spriteType = SpriteMeshType.Tight) 18 | { 19 | Sprite NewSprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0, 0), PixelsPerUnit, 0, spriteType); 20 | 21 | return NewSprite; 22 | } 23 | 24 | public static Texture2D LoadTexture(string FilePath) 25 | { 26 | Texture2D Tex2D; 27 | byte[] FileData; 28 | 29 | if (File.Exists(FilePath)) 30 | { 31 | FileData = File.ReadAllBytes(FilePath); 32 | Tex2D = new Texture2D(2, 2); 33 | if (Tex2D.LoadImage(FileData)) 34 | return Tex2D; 35 | } 36 | return null; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /WurstMod/Shared/Constants.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using BepInEx; 3 | 4 | namespace WurstMod.Shared 5 | { 6 | internal static class Constants 7 | { 8 | public static readonly string LegacyLevelsDirectory = Path.Combine(Paths.PluginPath, "LegacyCustomLevels"); 9 | public const string FilenameLevelData = "leveldata"; 10 | public const string FilenameLevelInfo = "info.json"; 11 | public const string FilenameLevelThumbnail = "thumb.png"; 12 | 13 | public const string RootObjectLevelName = "[LEVEL]"; 14 | 15 | public const string GamemodeTakeAndHold = "h3vr.take_and_hold"; 16 | public const string GamemodeSandbox = "h3vr.sandbox"; 17 | } 18 | } -------------------------------------------------------------------------------- /WurstMod/Shared/Enums.cs: -------------------------------------------------------------------------------- 1 | namespace WurstMod.Shared 2 | { 3 | public class Enums 4 | { 5 | public enum P_SosigEnemyID 6 | { 7 | None = -1, 8 | Misc_Dummy = 0, 9 | Misc_Elf = 1, 10 | M_Shotdog_Scout = 100, 11 | M_Shotdog_Ranger = 101, 12 | M_Shotdog_Sniper = 102, 13 | M_Shotdog_Riflewiener = 103, 14 | M_Shotdog_Officer = 104, 15 | M_Shotdog_SpecOps = 105, 16 | M_Shotdog_Markswiener = 106, 17 | M_Shotdog_Shield = 107, 18 | M_Shotdog_Heavy = 108, 19 | M_Shotdog_Breacher = 109, 20 | M_Shotdog_Guard = 110, 21 | M_MercWiener_Scout = 120, 22 | M_MercWiener_Ranger = 121, 23 | M_MercWiener_Sniper = 122, 24 | M_MercWiener_Riflewiener = 123, 25 | M_MercWiener_Officer = 124, 26 | M_MercWiener_SpecOps = 125, 27 | M_MercWiener_Markswiener = 126, 28 | M_MercWiener_Shield = 127, 29 | M_MercWiener_Heavy = 128, 30 | M_MercWiener_Breacher = 129, 31 | M_MercWiener_Guard = 130, 32 | M_GreaseGremlins_Scout = 140, 33 | M_GreaseGremlins_Ranger = 141, 34 | M_GreaseGremlins_Sniper = 142, 35 | M_GreaseGremlins_Riflewiener = 143, 36 | M_GreaseGremlins_Officer = 144, 37 | M_GreaseGremlins_SpecOps = 145, 38 | M_GreaseGremlins_Markswiener = 146, 39 | M_GreaseGremlins_Shield = 147, 40 | M_GreaseGremlins_Heavy = 148, 41 | M_GreaseGremlins_Breacher = 149, 42 | M_GreaseGremlins_Guard = 150, 43 | M_Popsicles_Scout = 160, 44 | M_Popsicles_Ranger = 161, 45 | M_Popsicles_Sniper = 162, 46 | M_Popsicles_Riflewiener = 163, 47 | M_Popsicles_Officer = 164, 48 | M_Popsicles_SpecOps = 165, 49 | M_Popsicles_Markswiener = 166, 50 | M_Popsicles_Shield = 167, 51 | M_Popsicles_Heavy = 168, 52 | M_Popsicles_Breacher = 169, 53 | M_Popsicles_Guard = 170, 54 | M_VeggieDawgs_Scout = 180, 55 | M_VeggieDawgs_Ranger = 181, 56 | M_VeggieDawgs_Sniper = 182, 57 | M_VeggieDawgs_Riflewiener = 183, 58 | M_VeggieDawgs_Officer = 184, 59 | M_VeggieDawgs_SpecOps = 185, 60 | M_VeggieDawgs_Markswiener = 186, 61 | M_VeggieDawgs_Shield = 187, 62 | M_VeggieDawgs_Heavy = 188, 63 | M_VeggieDawgs_Breacher = 189, 64 | M_VeggieDawgs_Guard = 190, 65 | W_Green_Guard = 200, 66 | W_Green_Patrol = 201, 67 | W_Green_Officer = 202, 68 | W_Green_Riflewiener = 203, 69 | W_Green_Grenadier = 204, 70 | W_Green_HeavyRiflewiener = 205, 71 | W_Green_Machinegunner = 206, 72 | W_Green_Flamewiener = 207, 73 | W_Green_Antitank = 208, 74 | W_Tan_Guard = 210, 75 | W_Tan_Patrol = 211, 76 | W_Tan_Officer = 212, 77 | W_Tan_Riflewiener = 213, 78 | W_Tan_Grenadier = 214, 79 | W_Tan_HeavyRiflewiener = 215, 80 | W_Tan_Machinegunner = 216, 81 | W_Tan_Flamewiener = 217, 82 | W_Tan_Antitank = 218, 83 | W_Brown_Guard = 220, 84 | W_Brown_Patrol = 221, 85 | W_Brown_Officer = 222, 86 | W_Brown_Riflewiener = 223, 87 | W_Brown_Grenadier = 224, 88 | W_Brown_HeavyRiflewiener = 225, 89 | W_Brown_Machinegunner = 226, 90 | W_Brown_Flamewiener = 227, 91 | W_Brown_Antitank = 228, 92 | W_Grey_Guard = 230, 93 | W_Grey_Patrol = 231, 94 | W_Grey_Officer = 232, 95 | W_Grey_Riflewiener = 233, 96 | W_Grey_Grenadier = 234, 97 | W_Grey_HeavyRiflewiener = 235, 98 | W_Grey_Machinegunner = 236, 99 | W_Grey_Flamewiener = 237, 100 | W_Grey_Antitank = 238, 101 | D_Gambler = 300, 102 | D_Bandito = 301, 103 | D_Gunfighter = 302, 104 | D_BountyHunter = 303, 105 | D_Sheriff = 304, 106 | D_Boss = 305, 107 | D_Sniper = 306, 108 | D_BountyHunterBoss = 307, 109 | J_Guard = 400, 110 | J_Patrol = 401, 111 | J_Grenadier = 402, 112 | J_Officer = 403, 113 | J_Commando = 404, 114 | J_Riflewiener = 405, 115 | J_Flamewiener = 406, 116 | J_Machinegunner = 407, 117 | J_Sniper = 408, 118 | H_BreadCrabZombie_Fast = 500, 119 | H_BreadCrabZombie_HEV = 501, 120 | H_BreadCrabZombie_Poison = 502, 121 | H_BreadCrabZombie_Standard = 503, 122 | H_BreadCrabZombie_Zombie = 504, 123 | H_CivicErection_Meathack = 505, 124 | H_CivicErection_Melee = 506, 125 | H_CivicErection_Pistol = 507, 126 | H_CivicErection_SMG = 508, 127 | H_OberwurstElite_AR2 = 509, 128 | H_OberwurstSoldier_Shotgun = 510, 129 | H_OberwurstSoldier_SMG = 511, 130 | H_OberwurstSoldier_SMGNade = 512, 131 | H_OberwurstSoldier_Sniper = 513, 132 | MF_RedHots_Demo = 600, 133 | MF_RedHots_Engineer = 601, 134 | MF_RedHots_Heavy = 602, 135 | MF_RedHots_Medic = 603, 136 | MF_RedHots_Pyro = 604, 137 | MF_RedHots_Scout = 605, 138 | MF_RedHots_Sniper = 606, 139 | MF_RedHots_Soldier = 607, 140 | MF_RedHots_Spy = 608, 141 | MF_BlueFranks_Demo = 610, 142 | MF_BlueFranks_Engineer = 611, 143 | MF_BlueFranks_Heavy = 612, 144 | MF_BlueFranks_Medic = 613, 145 | MF_BlueFranks_Pyro = 614, 146 | MF_BlueFranks_Scout = 615, 147 | MF_BlueFranks_Sniper = 616, 148 | MF_BlueFranks_Soldier = 617, 149 | MF_BlueFranks_Spy = 618, 150 | Gladiator_Hoplite = 700, 151 | Gladiator_Maximus = 701, 152 | Gladiator_Murmillo = 702, 153 | Gladiator_Porcus = 703, 154 | Gladiator_Secutor = 704, 155 | Gladiator_Thraex = 705, 156 | MountainMeat_Melee = 800, 157 | MountainMeat_Pistol = 801, 158 | MountainMeat_Rifle = 802, 159 | MountainMeat_Shotgun = 803, 160 | MountainMeat_SMG = 804, 161 | RW_Beefkicker = 900, 162 | RW_Boner = 901, 163 | RW_Driller = 902, 164 | RW_Floater = 903, 165 | RW_FunGuy = 904, 166 | RW_HamFister = 905, 167 | RW_Lemonhead = 906, 168 | RW_OldSmokey = 907, 169 | RW_Pig = 908, 170 | RW_Prick = 909, 171 | RW_RedLumberjack = 910, 172 | RW_Rot = 911, 173 | RW_Spurter = 912, 174 | RW_TheHung = 913, 175 | RWP_Cultist = 1000, 176 | RWP_PacSquad_Commander = 1001, 177 | RWP_PacSquad_Flanker = 1002, 178 | RWP_PacSquad_Sniper = 1003, 179 | RWP_PacSquad_Trooper = 1004, 180 | RWP_Prospector_Bar = 1005, 181 | RWP_Prospector_Pistol = 1006, 182 | RWP_Prospector_Rifle = 1007, 183 | RWP_Prospector_Shotgun = 1008, 184 | RWP_Skulker_Pistol = 1009, 185 | RWP_Skulker_Rifler = 1010, 186 | RWP_Skulker_Shotgun = 1011, 187 | RWNPC_00 = 1100, 188 | RWNPC_01 = 1101, 189 | RWNPC_02 = 1102, 190 | RWNPC_03 = 1103, 191 | RWNPC_04 = 1104, 192 | RWNPC_05 = 1105, 193 | RWNPC_06 = 1106, 194 | RWNPC_07 = 1107, 195 | RWNPC_08 = 1108, 196 | RWNPC_09 = 1109, 197 | RWNPC_10 = 1110, 198 | RWNPC_11 = 1111, 199 | RWNPC_12 = 1112, 200 | RWNPC_13 = 1113, 201 | RWNPC_14 = 1114, 202 | RWNPC_15 = 1115, 203 | RWNPC_16 = 1116, 204 | RWNPC_17 = 1117, 205 | RWNPC_18 = 1118, 206 | RWNPC_19 = 1119, 207 | RWNPC_20 = 1120, 208 | RWNPC_21 = 1121 209 | } 210 | } 211 | } -------------------------------------------------------------------------------- /WurstMod/Shared/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using UnityEngine; 6 | using UnityEngine.SceneManagement; 7 | 8 | namespace WurstMod.Shared 9 | { 10 | public static class Extensions 11 | { 12 | #region LINQ 13 | public static IEnumerable DistinctBy(this IEnumerable items, Func property) 14 | { 15 | return items.GroupBy(property).Select(x => x.First()); 16 | } 17 | 18 | public static IEnumerable AsEnumerable(this Transform transform) 19 | { 20 | if (transform != null) 21 | { 22 | return transform.Cast(); 23 | } 24 | else 25 | { 26 | return new List(); 27 | } 28 | } 29 | #endregion 30 | 31 | #region Reflection 32 | // Reflection shortcuts with caching. A smidge less verbose than InvokeMember. 33 | // Currently doesn't work for overloaded methods. 34 | // Cross that bridge when we come to it, I guess. 35 | private struct ReflectDef 36 | { 37 | public Type type; 38 | public string name; 39 | public ReflectDef(Type type, string name) 40 | { 41 | this.type = type; 42 | this.name = name; 43 | } 44 | } 45 | private static Dictionary methodCache = new Dictionary(); 46 | private static Dictionary fieldCache = new Dictionary(); 47 | 48 | public static object ReflectInvoke(this UnityEngine.Object target, string methodName, params object[] parameters) 49 | { 50 | MethodInfo method; 51 | ReflectDef def = new ReflectDef(target.GetType(), methodName); 52 | if (methodCache.ContainsKey(def)) 53 | { 54 | method = methodCache[def]; 55 | } 56 | else 57 | { 58 | method = target.GetType().GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance); 59 | methodCache[def] = method; 60 | } 61 | 62 | return method.Invoke(target, parameters); 63 | } 64 | 65 | public static T ReflectGet(this UnityEngine.Object target, string fieldName) 66 | { 67 | FieldInfo field; 68 | ReflectDef def = new ReflectDef(target.GetType(), fieldName); 69 | if (fieldCache.ContainsKey(def)) 70 | { 71 | field = fieldCache[def]; 72 | } 73 | else 74 | { 75 | field = target.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance); 76 | fieldCache[def] = field; 77 | } 78 | 79 | return (T)field.GetValue(target); 80 | } 81 | 82 | public static void ReflectSet(this UnityEngine.Object target, string fieldName, object value) 83 | { 84 | FieldInfo field; 85 | ReflectDef def = new ReflectDef(target.GetType(), fieldName); 86 | if (fieldCache.ContainsKey(def)) 87 | { 88 | field = fieldCache[def]; 89 | } 90 | else 91 | { 92 | field = target.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance); 93 | fieldCache[def] = field; 94 | } 95 | 96 | field.SetValue(target, value); 97 | } 98 | 99 | public static Type[] GetTypesSafe(this Assembly asm) 100 | { 101 | try 102 | { 103 | return asm.GetTypes(); 104 | } 105 | catch (ReflectionTypeLoadException e) 106 | { 107 | return e.Types.Where(t => t != null).ToArray(); 108 | } 109 | } 110 | #endregion 111 | 112 | #region Scene 113 | public static List GetAllGameObjectsInScene(this Scene scene) 114 | { 115 | List allGameObjects = new List(); 116 | List rootGameObjects = scene.GetRootGameObjects().ToList(); 117 | foreach (GameObject ii in rootGameObjects) 118 | { 119 | foreach (GameObject jj in ii.GetComponentsInChildren(true).Select(x => x.gameObject)) 120 | { 121 | allGameObjects.Add(jj); 122 | } 123 | } 124 | return allGameObjects; 125 | } 126 | 127 | public static void RefreshShader(this Material mat) 128 | { 129 | if (mat != null) 130 | { 131 | Shader refreshed = Shader.Find(mat.shader.name); 132 | if (refreshed != null) 133 | { 134 | mat.shader = Shader.Find(mat.shader.name); 135 | return; 136 | } 137 | Debug.LogError("WARNING: Failed to refresh shader \"" + mat.shader.name + "\", objects with this shader may render incorrectly."); 138 | } 139 | else 140 | { 141 | mat = new Material(Shader.Find("Standard")); 142 | Debug.LogError("WARNING: Attempted to refresh missing material, affected objects may render incorrectly."); 143 | } 144 | 145 | } 146 | 147 | public static T GetComponentBidirectional(this Component mb) where T : Component 148 | { 149 | // Easy, builtin for checking self and children. 150 | T found = mb.GetComponentInChildren(); 151 | if (found != null) return found; 152 | 153 | // Annoying and hacky, checking parents. 154 | Transform parent = mb.transform.parent; 155 | while (parent != null) 156 | { 157 | found = parent.GetComponent(); 158 | if (found != null) return found; 159 | parent = parent.parent; 160 | } 161 | return null; 162 | } 163 | #endregion 164 | 165 | #region Gizmos 166 | public static void GenericGizmoCube(Color color, Vector3 center, Vector3 size, Vector3 forward, params Transform[] markers) 167 | { 168 | Gizmos.color = color; 169 | foreach (Transform ii in markers) 170 | { 171 | Gizmos.matrix = ii.localToWorldMatrix; 172 | Gizmos.DrawCube(center, size); 173 | Gizmos.DrawLine(center, center + forward); 174 | } 175 | } 176 | 177 | public static void GenericGizmoCubeOutline(Color color, Vector3 center, Vector3 size, params Transform[] markers) 178 | { 179 | Gizmos.color = color; 180 | foreach (Transform ii in markers) 181 | { 182 | Gizmos.matrix = ii.localToWorldMatrix; 183 | Gizmos.DrawWireCube(center, size); 184 | } 185 | } 186 | 187 | public static void GenericGizmoSphere(Color color, Vector3 center, float radius, params Transform[] markers) 188 | { 189 | Gizmos.color = color; 190 | foreach (Transform ii in markers) 191 | { 192 | Gizmos.matrix = ii.localToWorldMatrix; 193 | Gizmos.DrawSphere(center, radius); 194 | } 195 | } 196 | 197 | public static void GenericGizmoSphereOutline(Color color, Vector3 center, float radius, params Transform[] markers) 198 | { 199 | Gizmos.color = color; 200 | foreach (Transform ii in markers) 201 | { 202 | Gizmos.matrix = ii.localToWorldMatrix; 203 | Gizmos.DrawWireSphere(center, radius); 204 | } 205 | } 206 | #endregion 207 | 208 | public static string Truncate(this string value, int maxLength) 209 | { 210 | if (string.IsNullOrEmpty(value)) return value; 211 | return value.Length <= maxLength ? value : value.Substring(0, maxLength); 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /WurstMod/Shared/LevelInfo.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using UnityEngine; 3 | using Valve.Newtonsoft.Json; 4 | #if !UNITY_EDITOR 5 | using Deli; 6 | using Deli.VFS; 7 | using FistVR; 8 | using Valve.VR.InteractionSystem.Sample; 9 | #endif 10 | 11 | namespace WurstMod.Shared 12 | { 13 | [JsonObject(MemberSerialization.OptIn)] 14 | public struct LevelInfo 15 | { 16 | [JsonProperty] public string SceneName; 17 | [JsonProperty] public string Author; 18 | [JsonProperty] public string Gamemode; 19 | [JsonProperty] public string Description; 20 | 21 | // This is a replacement for using the location of the level asset bundle as a unique identifier. 22 | public string Identifier => SceneName.Replace(" ", "").Truncate(16); 23 | 24 | #if !UNITY_EDITOR 25 | // We don't want this serialized 26 | public IDirectoryHandle Location; 27 | 28 | public Mod Mod; 29 | 30 | public IFileHandle AssetBundlePath => Location.GetFile(Constants.FilenameLevelData); 31 | public IFileHandle ThumbnailPath => Location.GetFile(Constants.FilenameLevelThumbnail); 32 | public IFileHandle LevelInfoPath => Location.GetFile(Constants.FilenameLevelInfo); 33 | 34 | public Texture2D Thumbnail 35 | { 36 | get 37 | { 38 | try 39 | { 40 | var stream = ThumbnailPath.OpenRead(); 41 | var buffer = new byte[stream.Length]; 42 | stream.Read(buffer, 0, buffer.Length); 43 | var tex = new Texture2D(0, 0); 44 | tex.LoadImage(buffer); 45 | return tex; 46 | } 47 | catch 48 | { 49 | return null; 50 | } 51 | } 52 | } 53 | 54 | public Sprite Sprite 55 | { 56 | get 57 | { 58 | Texture2D tex = Thumbnail; 59 | return tex != null ? Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), Vector2.one / 2) : null; 60 | } 61 | } 62 | 63 | private AssetBundle _cached; 64 | 65 | public AssetBundle AssetBundle 66 | { 67 | get 68 | { 69 | if (!_cached) 70 | { 71 | var stream = AssetBundlePath.OpenRead(); 72 | var buffer = new byte[stream.Length]; 73 | stream.Read(buffer, 0, buffer.Length); 74 | _cached = AssetBundle.LoadFromMemory(buffer); 75 | } 76 | 77 | return _cached; 78 | } 79 | } 80 | 81 | /// 82 | /// Creates a LevelInfo struct with the information given from a mod archive module 83 | /// 84 | /// The Deli.Mod the module originates from 85 | /// The module 86 | /// A LevelInfo from it 87 | public static LevelInfo? FromFrameworkMod(object source, object handle) 88 | { 89 | var mod = (Deli.Mod) source; 90 | var path = (IDirectoryHandle) handle; 91 | 92 | // Check if this level uses a json manifest (WM >= 2) 93 | var manifest = path.GetFile(Constants.FilenameLevelInfo); 94 | if (manifest != null) 95 | { 96 | // Load the level info from the mod archive 97 | var bytes = new StreamReader(manifest.OpenRead()).ReadToEnd(); 98 | var levelInfo = JsonConvert.DeserializeObject(bytes); 99 | 100 | // Set some vars and return it 101 | levelInfo.Location = path; 102 | levelInfo.Mod = mod; 103 | return levelInfo; 104 | } 105 | 106 | // Check if this level uses an info txt file (WM < 2) 107 | manifest = path.GetFile("info.txt"); 108 | if (manifest != null) 109 | { 110 | // This supports the older format 111 | var lines = new StreamReader(manifest.OpenRead()).ReadToEnd().Split('\n'); 112 | return new LevelInfo 113 | { 114 | SceneName = lines[0].Trim(), 115 | Author = lines[1].Trim(), 116 | Gamemode = path.Path.Contains("TakeAndHold") ? Constants.GamemodeTakeAndHold : Constants.GamemodeSandbox, 117 | Location = path, 118 | Mod = mod 119 | }; 120 | } 121 | 122 | // If somehow neither of the above two options match it's an invalid thing. 123 | if (mod.Info.Guid != "wurstmodders.wurstmod.legacy") 124 | mod.Logger.LogError($"Level at {path} does not contain a valid manifest!"); 125 | return null; 126 | } 127 | #endif 128 | /// 129 | /// This should really only be used inside the Unity Editor 130 | /// 131 | public void ToFile(string location) 132 | { 133 | // Make sure the directory exists 134 | if (!Directory.Exists(location)) Directory.CreateDirectory(location); 135 | 136 | // Then write the serialized data 137 | File.WriteAllText(Path.Combine(location, Constants.FilenameLevelInfo), JsonConvert.SerializeObject(this)); 138 | } 139 | 140 | } 141 | } -------------------------------------------------------------------------------- /WurstMod/UnityEditor/Exporter.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_EDITOR 2 | using System.Collections.Generic; 3 | using UnityEditor; 4 | using UnityEngine; 5 | using UnityEngine.SceneManagement; 6 | using WurstMod.MappingComponents.Generic; 7 | using WurstMod.Shared; 8 | using WurstMod.UnityEditor.SceneExporters; 9 | 10 | namespace WurstMod.UnityEditor 11 | { 12 | public static class Exporter 13 | { 14 | /// 15 | /// In exporting we have the following steps: 16 | /// Run ComponentProxy OnExport() 17 | /// Find & call the SceneExporter for the given game mode 18 | /// If no errors were returned, create the bundle 19 | /// 20 | public static void Export(Scene scene, ExportErrors err) 21 | { 22 | // Get the root objects in the scene 23 | var roots = scene.GetRootGameObjects(); 24 | 25 | // Make sure there's only one and it's name is correct 26 | if (roots.Length != 1 || roots[0].name != Constants.RootObjectLevelName) 27 | { 28 | err.AddError($"You must only have one root object in your scene and it must be named '{Constants.RootObjectLevelName}'"); 29 | return; 30 | } 31 | 32 | // Make sure it has the CustomScene component. If not, error out 33 | var sceneRoot = roots[0].GetComponent(); 34 | if (!sceneRoot) 35 | { 36 | err.AddError($"Your root object must have the {nameof(CustomScene)} component on it!"); 37 | return; 38 | } 39 | 40 | // Find the exporter class. If none is found, error out 41 | SceneExporter.RefreshLoadedSceneExporters(); 42 | var exporter = SceneExporter.GetExporterForGamemode(sceneRoot.Gamemode); 43 | if (exporter == null) 44 | { 45 | err.AddError($"Could not find an exporter class for the gamemode '{sceneRoot.Gamemode}'. Are you missing an assembly?"); 46 | return; 47 | } 48 | 49 | // We have an exporter class, so let it handle the rest of validating the scene 50 | exporter.Validate(scene, sceneRoot, err); 51 | 52 | // Check for errors after validating 53 | if (err.HasErrors) 54 | { 55 | EditorUtility.DisplayDialog("Validation failed.", "There were errors while validating the scene. Check console for more details.", "Oops"); 56 | return; 57 | } 58 | 59 | // Check for warnings, and give the option to continue 60 | if (err.HasWarnings) 61 | { 62 | var result = EditorUtility.DisplayDialog("Validation succeeded with warnings", "There were warnings while validating the scene. This will not prevent you from continuing, but it is recommended you cancel and correct them", "Continue", "Cancel"); 63 | if (!result) return; 64 | } 65 | 66 | // Now the scene is validated we can export. 67 | exporter.Export(); 68 | 69 | Debug.Log("Scene exported!"); 70 | } 71 | } 72 | 73 | public class ExportErrors 74 | { 75 | // List fields to store the messages 76 | public readonly List Debug = new List(); 77 | public readonly List Warnings = new List(); 78 | public readonly List Errors = new List(); 79 | 80 | // Some boolean properties for easy checking 81 | public bool HasDebug => Debug.Count != 0; 82 | public bool HasErrors => Warnings.Count != 0; 83 | public bool HasWarnings => Errors.Count != 0; 84 | 85 | // Methods to add to the lists 86 | public void AddDebug(string message, Object context = null) 87 | { 88 | Debug.Add(message); 89 | UnityEngine.Debug.Log(message, context); 90 | } 91 | 92 | public void AddWarning(string message, Object context = null) 93 | { 94 | Warnings.Add(message); 95 | UnityEngine.Debug.LogWarning(message, context); 96 | } 97 | 98 | public void AddError(string message, Object context = null) 99 | { 100 | Errors.Add(message); 101 | UnityEngine.Debug.LogError(message, context); 102 | } 103 | } 104 | } 105 | #endif -------------------------------------------------------------------------------- /WurstMod/UnityEditor/ExporterWindow.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_EDITOR 2 | using System; 3 | using System.Linq; 4 | using UnityEditor; 5 | using UnityEngine; 6 | using UnityEngine.SceneManagement; 7 | using WurstMod.MappingComponents.Generic; 8 | using WurstMod.UnityEditor.SceneExporters; 9 | 10 | namespace WurstMod.UnityEditor 11 | { 12 | public class ExporterWindow : EditorWindow 13 | { 14 | private static CustomScene _scene; 15 | 16 | [MenuItem("Wurst Mod/Export Window")] 17 | public static void Open() 18 | { 19 | _scene = GetRoot(); 20 | SceneExporter.RefreshLoadedSceneExporters(); 21 | GetWindow("WurstMod Export"); 22 | } 23 | 24 | private static CustomScene GetRoot() 25 | { 26 | var roots = SceneManager.GetActiveScene().GetRootGameObjects(); 27 | return roots.Length == 1 ? roots[0].GetComponent() : null; 28 | } 29 | 30 | private void OnGUI() 31 | { 32 | // If scene is null, check again. Scene changes and exports can cause check to fail when it shouldn't. 33 | if (!_scene) _scene = GetRoot(); 34 | 35 | if (!_scene) 36 | { 37 | GUILayout.Label("Could not find Custom Scene component on root game object.", EditorStyles.boldLabel); 38 | return; 39 | } 40 | 41 | // Draw the meta data fields 42 | GUILayout.Label("Scene metadata", EditorStyles.boldLabel); 43 | _scene.SceneName = EditorGUILayout.TextField("Scene Name", _scene.SceneName); 44 | _scene.Author = EditorGUILayout.TextField("Author", _scene.Author); 45 | _scene.Description = EditorGUILayout.TextField("Description", _scene.Description, GUILayout.MaxHeight(75)); 46 | 47 | // Game mode stuff 48 | _scene.Gamemode = DrawGamemode(_scene.Gamemode); 49 | 50 | if (GUILayout.Button("Export Scene")) 51 | { 52 | var scene = SceneManager.GetActiveScene(); 53 | var err = new ExportErrors(); 54 | Exporter.Export(scene, err); 55 | } 56 | } 57 | 58 | private string DrawGamemode(string current) 59 | { 60 | // If we haven't yet registered any types, do it now. 61 | // This will need to be done when Unity reloads assemblies, so unfortunately more often than I'd like 62 | if (SceneExporter.RegisteredSceneExporters == null) SceneExporter.RefreshLoadedSceneExporters(); 63 | var exporters = (SceneExporter.RegisteredSceneExporters ?? new SceneExporter[0]).ToArray(); 64 | var choices = exporters.Select(x => x.GamemodeId).ToArray(); 65 | 66 | var currentIndex = Array.IndexOf(choices, current); 67 | 68 | // If the game mode isn't valid, set it to zero and let the user re-pick 69 | if (currentIndex == -1) currentIndex = 0; 70 | 71 | EditorGUILayout.BeginHorizontal(); 72 | EditorGUILayout.LabelField("Gamemode"); 73 | var newIndex = EditorGUILayout.Popup(currentIndex, choices); 74 | EditorGUILayout.EndHorizontal(); 75 | if (currentIndex != newIndex) _scene.ExtraData = new CustomScene.StringKeyValue[0]; 76 | _scene.ExtraData = exporters[newIndex].OnExporterGUI(_scene.ExtraData); 77 | 78 | return choices[newIndex]; 79 | } 80 | } 81 | } 82 | #endif -------------------------------------------------------------------------------- /WurstMod/UnityEditor/SceneExporters/SandboxExporter.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_EDITOR 2 | using UnityEngine.SceneManagement; 3 | using WurstMod.MappingComponents.Generic; 4 | using WurstMod.MappingComponents.Sandbox; 5 | using WurstMod.Shared; 6 | 7 | namespace WurstMod.UnityEditor.SceneExporters 8 | { 9 | public class SandboxExporter : SceneExporter 10 | { 11 | public override string GamemodeId => Constants.GamemodeSandbox; 12 | 13 | public override void Validate(Scene scene, CustomScene root, ExportErrors err) 14 | { 15 | // Let the base validate 16 | base.Validate(scene, root, err); 17 | 18 | // Check for a spawn 19 | RequiredComponents(1, 1); 20 | } 21 | } 22 | } 23 | #endif -------------------------------------------------------------------------------- /WurstMod/UnityEditor/SceneExporters/SceneExporter.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_EDITOR 2 | using FistVR; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | using UnityEditor; 8 | using UnityEditor.SceneManagement; 9 | using UnityEngine; 10 | using UnityEngine.SceneManagement; 11 | using Valve.VR.InteractionSystem; 12 | using WurstMod.MappingComponents; 13 | using WurstMod.MappingComponents.Generic; 14 | using WurstMod.Shared; 15 | 16 | namespace WurstMod.UnityEditor.SceneExporters 17 | { 18 | public abstract class SceneExporter 19 | { 20 | private Scene _scene; 21 | private CustomScene _root; 22 | private ExportErrors _err; 23 | 24 | /// 25 | /// This is implemented by the deriving class and is a unique identifier for a game mode. 26 | /// 27 | public abstract string GamemodeId { get; } 28 | 29 | /// 30 | /// This is called to validate the scene and ensure it can be exported. 31 | /// If you override this, make sure to call base.Validate(scene, root, err)! (Preferably before your code) 32 | /// Also, do NOT return early from this method unless absolutely necessary. Validate as many thing as you can. 33 | /// 34 | /// The scene to export 35 | /// The root game object 36 | /// An object used to inform the exporter what's happened 37 | public virtual void Validate(Scene scene, CustomScene root, ExportErrors err) 38 | { 39 | // Save these for later. 40 | _scene = scene; 41 | _root = root; 42 | _err = err; 43 | 44 | // Check for NavMesh and Occlusion data 45 | // These aren't *required* so they will only be warnings 46 | if (!File.Exists(Path.GetDirectoryName(scene.path) + "/" + scene.name + "/NavMesh.asset")) err.AddWarning("Scene is missing NavMesh data!"); 47 | if (!File.Exists(Path.GetDirectoryName(scene.path) + "/" + scene.name + "/OcclusionCullingData.asset")) err.AddWarning("Scene is missing Occlusion Culling Data!"); 48 | 49 | // Let the proxied components know we're about to export 50 | foreach (var proxy in scene.GetRootGameObjects().SelectMany(x => x.GetComponentsInChildren())) proxy.OnExport(err); 51 | } 52 | 53 | /// 54 | /// This is called by the exporter window to let the game mode include custom data 55 | /// IMPORTANT: You should probably check that the array is of correct length. 56 | /// 57 | public virtual CustomScene.StringKeyValue[] OnExporterGUI(CustomScene.StringKeyValue[] customData) 58 | { 59 | if (customData == null || customData.Length != 0) customData = new CustomScene.StringKeyValue[0]; 60 | return customData; 61 | } 62 | 63 | /// 64 | /// This method is called after validate and will actually export the scene to files. 65 | /// 66 | public virtual void Export() 67 | { 68 | // Make sure the user saves their scene first 69 | if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) return; 70 | 71 | // Configure the build 72 | var buildOptions = BuildAssetBundleOptions.ChunkBasedCompression; 73 | var build = default(AssetBundleBuild); 74 | build.assetBundleName = Constants.FilenameLevelData; 75 | build.assetNames = new[] {_scene.path}; 76 | 77 | // Create a temporary folder to hold the build so if it fails nothing gets overwritten 78 | var location = $"AssetBundles/{_scene.name}/"; 79 | 80 | // Delete the folder if it exists already 81 | Debug.Log("Removing previous export..."); 82 | if (Directory.Exists(location)) 83 | { 84 | foreach (string path in Directory.GetFiles(location, "*.*", SearchOption.AllDirectories)) 85 | { 86 | File.Delete(path); 87 | } 88 | } 89 | 90 | // Create a LevelInfo object and save it 91 | Debug.Log("Writing level info..."); 92 | var levelInfo = new LevelInfo 93 | { 94 | SceneName = _root.SceneName, 95 | Author = _root.Author, 96 | Gamemode = _root.Gamemode, 97 | Description = _root.Description 98 | }; 99 | levelInfo.ToFile(location); 100 | 101 | // Export the asset bundle 102 | Debug.Log("Creating asset bundle..."); 103 | BuildPipeline.BuildAssetBundles(location, new[] {build}, buildOptions, BuildTarget.StandaloneWindows64); 104 | 105 | // Delete unnecessary files. 106 | Debug.Log("Cleaning up..."); 107 | var toDelete = new[] 108 | { 109 | Path.Combine(location, $"{Constants.FilenameLevelData}.manifest"), 110 | Path.Combine(location, _scene.name), 111 | Path.Combine(location, $"{_scene.name}.manifest") 112 | }; 113 | foreach (var file in toDelete) 114 | if (File.Exists(file)) 115 | File.Delete(file); 116 | } 117 | 118 | /// 119 | /// Simple helper function to check if the number of components of a certain type is between two numbers 120 | /// 121 | /// The minimum number 122 | /// The maximum number 123 | /// If invalid, produces a warning instead of an error 124 | /// Custom message to log 125 | /// The type of the component 126 | protected void RequiredComponents(int min, int max = int.MaxValue, bool warning = false, string message = null) 127 | { 128 | var count = _root.GetComponentsInChildren().Length; 129 | if (min <= count && count <= max) return; 130 | 131 | var msg = min == max ? $"{min}" : max == int.MaxValue ? $"at least {min}" : $"{min} - {max}"; 132 | if (warning) _err.AddWarning(message ?? $"Your scene contains {count} {typeof(T).Name}. Recommended number is {msg}"); 133 | else _err.AddError(message ?? $"Your scene contains {count} {typeof(T).Name}. Required number is {msg}"); 134 | } 135 | 136 | 137 | // Array of currently registered scene exporters 138 | public static SceneExporter[] RegisteredSceneExporters; 139 | 140 | /// 141 | /// Simple method to re-discover and instantiate scene exporters. 142 | /// 143 | public static void RefreshLoadedSceneExporters() => RegisteredSceneExporters = EnumerateExporterTypes().Select(x => Activator.CreateInstance(x) as SceneExporter).ToArray(); 144 | 145 | /// 146 | /// Enumerates all scene exporters across all loaded assemblies. You should probably use the 147 | /// RegisteredSceneLoaders variable instead of this as it's expensive. 148 | /// 149 | /// All scene exporters across all loaded assemblies 150 | public static IEnumerable EnumerateExporterTypes() 151 | { 152 | return AppDomain.CurrentDomain.GetAssemblies().SelectMany(x => x.GetTypesSafe()) 153 | .Where(x => x.IsSubclassOf(typeof(SceneExporter))); 154 | } 155 | 156 | /// 157 | /// Returns an exporter object for the provided game mode 158 | /// 159 | /// The gamemode 160 | /// The exporter object 161 | public static SceneExporter GetExporterForGamemode(string gamemode) => RegisteredSceneExporters.FirstOrDefault(x => x.GamemodeId == gamemode); 162 | } 163 | } 164 | #endif -------------------------------------------------------------------------------- /WurstMod/UnityEditor/SceneExporters/TakeAndHoldExporter.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_EDITOR 2 | using System; 3 | using UnityEditor; 4 | using UnityEngine.SceneManagement; 5 | using WurstMod.MappingComponents.Generic; 6 | using WurstMod.MappingComponents.TakeAndHold; 7 | using WurstMod.Shared; 8 | 9 | namespace WurstMod.UnityEditor.SceneExporters 10 | { 11 | public class TakeAndHoldExporter : SceneExporter 12 | { 13 | public override string GamemodeId => Constants.GamemodeTakeAndHold; 14 | 15 | public override void Validate(Scene scene, CustomScene root, ExportErrors err) 16 | { 17 | // Base validate 18 | base.Validate(scene, root, err); 19 | 20 | // Check for all required components 21 | RequiredComponents(1, 1); 22 | RequiredComponents(1, 1); 23 | RequiredComponents(2); 24 | RequiredComponents(3); 25 | RequiredComponents(0, 1); 26 | } 27 | 28 | public override CustomScene.StringKeyValue[] OnExporterGUI(CustomScene.StringKeyValue[] customData) 29 | { 30 | if (customData == null || customData.Length != 1) customData = new[] 31 | { 32 | new CustomScene.StringKeyValue {Key = "HoldOrder"}, 33 | }; 34 | 35 | customData[0].Value = EditorGUILayout.TextField("Hold Order", customData[0].Value); 36 | 37 | return customData; 38 | } 39 | } 40 | } 41 | #endif -------------------------------------------------------------------------------- /WurstMod/WurstMod.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {43E34E28-2C7E-42F4-92CA-C731AECFF593} 8 | Library 9 | Properties 10 | WurstMod 11 | WurstMod 12 | v3.5 13 | 512 14 | true 15 | 16 | 17 | 18 | 19 | true 20 | full 21 | false 22 | bin\Debug\ 23 | UNITY_EDITOR;DEBUG;TRACE 24 | prompt 25 | 4 26 | 649 27 | true 28 | 29 | 30 | full 31 | false 32 | bin\Release\ 33 | TRACE 34 | prompt 35 | 4 36 | 649 37 | true 38 | true 39 | 40 | 41 | Always 42 | 43 | 44 | 45 | ..\packages\HarmonyX.2.1.1\lib\net35\0Harmony.dll 46 | 47 | 48 | ..\packages\H3VR.GameLibs.0.111.10\lib\net35\Assembly-CSharp.dll 49 | 50 | 51 | ..\packages\H3VR.GameLibs.0.111.10\lib\net35\Assembly-CSharp-firstpass.dll 52 | 53 | 54 | ..\packages\BepInEx.BaseLib.5.4.0\lib\net35\BepInEx.dll 55 | 56 | 57 | ..\packages\Deli.Newtonsoft.Json.12.0.3\lib\net35\Deli.Newtonsoft.Json.dll 58 | 59 | 60 | ..\packages\Deli.Patcher.0.4.2\lib\net35\Deli.Patcher.dll 61 | 62 | 63 | ..\packages\Deli.0.4.2\lib\net35\Deli.Setup.dll 64 | 65 | 66 | ..\packages\DotNetZip.1.12.0\lib\net20\DotNetZip.dll 67 | 68 | 69 | ..\packages\Mono.Cecil.0.10.4\lib\net35\Mono.Cecil.dll 70 | 71 | 72 | ..\packages\Mono.Cecil.0.10.4\lib\net35\Mono.Cecil.Mdb.dll 73 | 74 | 75 | ..\packages\Mono.Cecil.0.10.4\lib\net35\Mono.Cecil.Pdb.dll 76 | 77 | 78 | ..\packages\Mono.Cecil.0.10.4\lib\net35\Mono.Cecil.Rocks.dll 79 | 80 | 81 | ..\packages\MonoMod.RuntimeDetour.20.11.5.1\lib\net35\MonoMod.RuntimeDetour.dll 82 | 83 | 84 | ..\packages\MonoMod.Utils.20.11.5.1\lib\net35\MonoMod.Utils.dll 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | C:\Program Files\Unity\5.6.3p4\Editor\Data\Managed\UnityEditor.dll 94 | 95 | 96 | ..\packages\UnityEngine.Core.5.6.0\lib\net35\UnityEngine.dll 97 | 98 | 99 | ..\packages\H3VR.GameLibs.0.111.10\lib\net35\UnityEngine.UI.dll 100 | 101 | 102 | ..\packages\H3VR.GameLibs.0.111.10\lib\net35\Valve.Newtonsoft.Json.dll 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | PreserveNewest 160 | 161 | 162 | Always 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | $(SolutionDir)pdb2mdb.exe $(TargetPath) 171 | 172 | 173 | 174 | 175 | This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105.The missing file is {0}. 176 | 177 | 178 | 179 | -------------------------------------------------------------------------------- /WurstMod/legacyManifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "WurstMod Legacy", 3 | "authors": [ 4 | "Nathan Gill", 5 | "The authors of all the maps that this loads lmao" 6 | ], 7 | "guid": "wurstmodders.wurstmod.legacy", 8 | "version": "1.0.0", 9 | "require": "0.3.1", 10 | "dependencies": { 11 | "wurstmodders.wurstmod": "2.0.4" 12 | }, 13 | "assets": { 14 | "setup": { 15 | "**/*.dll": "deli:assembly", 16 | "**/": "wurstmodders.wurstmod:level" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /WurstMod/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "WurstMod", 3 | "authors": [ 4 | "Koba", 5 | "Nathan Gill" 6 | ], 7 | "guid": "wurstmodders.wurstmod", 8 | "source_url": "https://github.com/WurstModders/WurstMod", 9 | "version": "2.2.5", 10 | "require": "0.3.0", 11 | "dependencies": {}, 12 | "assets": { 13 | "setup": { 14 | "WurstMod.dll": "deli:assembly", 15 | "Debug Sandbox/": "wurstmodders.wurstmod:level", 16 | "Debug TNH/": "wurstmodders.wurstmod:level" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /WurstMod/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /WurstModCodeGen/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /WurstModCodeGen/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace WurstModCodeGen 7 | { 8 | public static class Extensions 9 | { 10 | /// 11 | /// This is a good-enough stable hash function that should be fully portable. 12 | /// https://stackoverflow.com/questions/36845430/persistent-hashcode-for-strings 13 | /// 14 | public static int GetStableHashCode(this string str) 15 | { 16 | unchecked 17 | { 18 | int hash1 = 5381; 19 | int hash2 = hash1; 20 | 21 | for (int i = 0; i < str.Length && str[i] != '\0'; i += 2) 22 | { 23 | hash1 = ((hash1 << 5) + hash1) ^ str[i]; 24 | if (i == str.Length - 1 || str[i + 1] == '\0') 25 | break; 26 | hash2 = ((hash2 << 5) + hash2) ^ str[i + 1]; 27 | } 28 | 29 | return hash1 + (hash2 * 1566083941); 30 | } 31 | } 32 | 33 | public static IEnumerable Zip35Deferred( 34 | this IEnumerable seqA, IEnumerable seqB, Func func) 35 | { 36 | using (var iteratorA = seqA.GetEnumerator()) 37 | using (var iteratorB = seqB.GetEnumerator()) 38 | { 39 | while (iteratorA.MoveNext() && iteratorB.MoveNext()) 40 | { 41 | yield return func(iteratorA.Current, iteratorB.Current); 42 | } 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /WurstModCodeGen/Generator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text.RegularExpressions; 6 | 7 | namespace WurstModCodeGen 8 | { 9 | /// 10 | /// This class generates a bunch of Enums! These enums should never break backwards compatibility as new objects are 11 | /// added to the game, because they are indexed by a stable hash. This is necessary because if an enum value changes, 12 | /// Unity will remember the index, not the name. Suddenly, an AKM is a can of tuna. This is bad. 13 | /// 14 | class Generator 15 | { 16 | static readonly string UnpackedPath = @"../../../uTinyExport"; 17 | static readonly string ClassName = "ResourceDefs"; 18 | static readonly string DestinationPath = $@"../../../WurstMod/Shared/{ClassName}.cs"; 19 | static readonly string AnvilEnumName = "AnvilAsset"; 20 | 21 | static string[] allFiles; 22 | static List allHashes = new List(); 23 | 24 | static void Main(string[] args) 25 | { 26 | if (!Directory.Exists(UnpackedPath)) 27 | { 28 | Console.WriteLine( 29 | @"This utility is used to generate certain enums from the unpacked source of the game, 30 | which will be automatically updated in the main WurstMod project. 31 | 32 | Using this utility requires an unpacked version of H3VR to be placed in a uTinyExport 33 | folder at the root of the repository."); 34 | Console.ReadLine(); 35 | } 36 | else 37 | { 38 | GetAllFiles(); 39 | string result = Generate(); 40 | result = result.Replace("\r", ""); 41 | result = result.Replace("\n", "\r\n"); 42 | File.WriteAllText(DestinationPath, result); 43 | 44 | if (allHashes.Count() != allHashes.Distinct().Count()) 45 | { 46 | var dupes = allHashes.GroupBy(x => x); 47 | foreach (var grp in dupes) 48 | { 49 | if (grp.Count() > 1) Console.WriteLine(grp.Key); 50 | } 51 | 52 | Console.WriteLine("Hash overlap happened. Backwards compatibility may be nuked."); 53 | Console.ReadLine(); 54 | } 55 | } 56 | } 57 | 58 | static void GetAllFiles() 59 | { 60 | allFiles = Directory.GetFiles(UnpackedPath, "*.*", SearchOption.AllDirectories); 61 | } 62 | 63 | static string Generate() 64 | { 65 | // Anvil 66 | string[] sosigBodyRegion = 67 | GeneratePartialRegion("SOSIGBODY__", "osigBody", "[SZ]osigBody_", AnvilEnumName, "objectids"); 68 | string[] sosigAccessoryRegion = GeneratePartialRegion("SOSIGACCESSORY__", "accessory", 69 | "[sS]osig[aA]ccessory_", AnvilEnumName, "objectids"); 70 | string[] sosigMeleeRegion = 71 | GeneratePartialRegion("SOSIGMELEE__", "SosigMelee", "SosigMelee_", AnvilEnumName, "objectids"); 72 | string[] sosigGunRegion = 73 | GeneratePartialRegion("SOSIGGUN__", "Sosiggun", "Sosiggun_", AnvilEnumName, "objectids"); 74 | string[] weaponStuffRegion = 75 | GeneratePartialRegion("WEAPONRY__", "weaponry_", @".*\\", AnvilEnumName, "objectids"); 76 | string[] rotwienerRegion = 77 | GeneratePartialRegion("ROTWIENER__", "", @".*\\", AnvilEnumName, "_returnrotwieners"); 78 | string[] powerupRegion = 79 | GeneratePartialRegion("POWERUP__", "PowerUpMeat", @".*\\", AnvilEnumName, "objectids"); 80 | 81 | // Other 82 | string pMatRegion = GenerateRegion("PMat", "PMat_", "PMat", "pmaterialdefinitions"); 83 | string matDefRegion = GenerateRegion("", @".*\\", "MatDef", "matdefs"); 84 | 85 | string output = 86 | $@"using System.Collections.Generic; 87 | 88 | namespace WurstMod.Shared 89 | {{ 90 | public static class {ClassName} 91 | {{ 92 | #region {AnvilEnumName} 93 | public enum {AnvilEnumName} 94 | {{ 95 | {sosigBodyRegion[0]}, 96 | {sosigAccessoryRegion[0]}, 97 | {sosigMeleeRegion[0]}, 98 | {sosigGunRegion[0]}, 99 | {weaponStuffRegion[0]}, 100 | {rotwienerRegion[0]}, 101 | {powerupRegion[0]} 102 | }} 103 | 104 | public static readonly Dictionary<{AnvilEnumName}, string> {AnvilEnumName}Resources = new Dictionary<{AnvilEnumName}, string>() 105 | {{ 106 | {sosigBodyRegion[1]}, 107 | {sosigAccessoryRegion[1]}, 108 | {sosigMeleeRegion[1]}, 109 | {sosigGunRegion[1]}, 110 | {weaponStuffRegion[1]}, 111 | {rotwienerRegion[1]}, 112 | {powerupRegion[1]} 113 | }}; 114 | #endregion 115 | {pMatRegion} 116 | {matDefRegion} 117 | }} 118 | }} 119 | "; 120 | 121 | return output; 122 | } 123 | 124 | static string[] GenericEnumDictifier(string fileFilter, string filePattern, string enumName, string folder, 125 | string prefix = "") 126 | { 127 | // Helper: Remove unwanted characters and ensure format is valid for C# enum declarations. 128 | string FormatEnum(string str) 129 | { 130 | char[] toRemove = {' ', '-', '+'}; 131 | foreach (string ii in toRemove.Select(x => x.ToString())) 132 | { 133 | str = str.Replace(ii, "_"); 134 | } 135 | 136 | str = prefix + str; 137 | if (char.IsDigit(str[0])) str = "_" + str; 138 | return str; 139 | } 140 | 141 | // Helper: Concatenate a string's hashcode onto the end of it like so: 142 | // {STRING} = {HASHCODE} 143 | // This provides a convenient way to make enums that are very unlikely 144 | // to be shifted around or broken as new elements are added. 145 | string[] OrderEnum(string[] values, bool countInAllHashes = false) 146 | { 147 | int[] orders = values.Select(x => x.GetStableHashCode()).ToArray(); 148 | if (countInAllHashes) 149 | { 150 | allHashes.AddRange(orders); 151 | } 152 | 153 | if (orders.Count() != orders.Distinct().Count()) 154 | { 155 | Console.WriteLine("Hash overlap happened. Backwards compatibility may be nuked."); 156 | Console.ReadLine(); 157 | } 158 | 159 | string[] results = new string[values.Length]; 160 | for (int ii = 0; ii < values.Length; ii++) results[ii] = $"{values[ii]} = {orders[ii]}"; 161 | 162 | return results; 163 | } 164 | 165 | 166 | string[] relevantFiles = allFiles 167 | .Where(x => x.ToLower().Contains(fileFilter.ToLower()) && Path.GetExtension(x) == ".asset").ToArray(); 168 | 169 | Regex fileRegex = new Regex($@".*?({folder}.*?{filePattern}(.*?))\.asset"); 170 | Match[] relevantMatches = relevantFiles.Select(x => fileRegex.Match(x)).ToArray(); 171 | relevantMatches = relevantMatches.Where(x => x.Groups[1].Success && x.Groups[2].Success).ToArray(); 172 | 173 | string[] enumValues = relevantMatches.Select(x => FormatEnum(x.Groups[2].ToString())).ToArray(); 174 | string[] enumValuesWithOrders = OrderEnum(enumValues, prefix != ""); 175 | string[] pathValues = relevantMatches.Select(x => x.Groups[1].ToString()).ToArray(); 176 | 177 | string enumFinal = string.Join(",\n ", enumValuesWithOrders.OrderBy(x => x).ToArray()); 178 | string pathFinal = string.Join(",\n ", 179 | enumValues.Zip35Deferred( 180 | pathValues, 181 | (e, p) => new {EnumVal = e, PathVal = p} 182 | ) 183 | .OrderBy(x => x.EnumVal) 184 | .Select(x => "{ " + enumName + "." + x.EnumVal + ", @\"" + x.PathVal + "\" }").ToArray()); 185 | 186 | string[] output = new string[] {enumFinal, pathFinal}; 187 | 188 | return output; 189 | } 190 | 191 | static string GenerateRegion(string fileFilter, string filePattern, string enumName, string folder) 192 | { 193 | string[] enumDict = GenericEnumDictifier(fileFilter, filePattern, enumName, folder); 194 | 195 | string output = 196 | $@" 197 | #region {enumName} 198 | public enum {enumName} 199 | {{ 200 | {enumDict[0]} 201 | }} 202 | 203 | public static readonly Dictionary<{enumName}, string> {enumName}Resources = new Dictionary<{enumName}, string>() 204 | {{ 205 | {enumDict[1]} 206 | }}; 207 | #endregion"; 208 | 209 | return output; 210 | } 211 | 212 | static string[] GeneratePartialRegion(string prefix, string fileFilter, string filePattern, string enumName, 213 | string folder) 214 | { 215 | return GenericEnumDictifier(fileFilter, filePattern, enumName, folder, prefix); 216 | } 217 | } 218 | } -------------------------------------------------------------------------------- /WurstModCodeGen/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("WurstModCodeGen")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("WurstModCodeGen")] 13 | [assembly: AssemblyCopyright("Copyright © 2020")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("3291ad19-a7c7-497f-bffa-2d4f93d3f50f")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /WurstModCodeGen/WurstModCodeGen.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {3291AD19-A7C7-497F-BFFA-2D4F93D3F50F} 8 | Exe 9 | WurstModCodeGen 10 | WurstModCodeGen 11 | v3.5 12 | 512 13 | true 14 | true 15 | 16 | 17 | AnyCPU 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | AnyCPU 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /WurstModWorkbench.unitypackage: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WurstModders/WurstMod/d6a8bf2e72230c5a45cafe4750206e2b1ff9220d/WurstModWorkbench.unitypackage -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /pdb2mdb.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WurstModders/WurstMod/d6a8bf2e72230c5a45cafe4750206e2b1ff9220d/pdb2mdb.exe --------------------------------------------------------------------------------