Add gesture support to OpenXR on WMR

Add gesture support to OpenXR on WMR

--- Assets/MRTK/Providers/OpenXR/Scripts/OpenXRDeviceManager.cs
+++ Assets/MRTK/Providers/OpenXR/Scripts/OpenXRDeviceManager.cs
@@ -11,6 +11,11 @@
 #if UNITY_OPENXR
 using UnityEngine.XR.OpenXR;
 #endif // UNITY_OPENXR
+
+#if MSFT_OPENXR_0_9_4_OR_NEWER && WINDOWS_UWP
+using Microsoft.MixedReality.OpenXR;
+using Microsoft.MixedReality.Toolkit.Windows.Input;
+#endif // MSFT_OPENXR_0_9_4_OR_NEWER && WINDOWS_UWP

 namespace Microsoft.MixedReality.Toolkit.XRSDK.OpenXR
 {
@@ -41,6 +46,22 @@
             false;
 #endif // UNITY_OPENXR

+#if MSFT_OPENXR_0_9_4_OR_NEWER && WINDOWS_UWP
+        private GestureRecognizer gestureRecognizer;
+        private GestureRecognizer navigationGestureRecognizer;
+        private GestureEventData eventData;
+        private AutoStartBehavior autoStartBehavior;
+        private WindowsGestureSettings gestureSettings;
+        private WindowsGestureSettings navigationSettings;
+        private WindowsGestureSettings railsNavigationSettings;
+        private bool useRailsNavigation;
+
+        private MixedRealityInputAction holdAction = MixedRealityInputAction.None;
+        private MixedRealityInputAction navigationAction = MixedRealityInputAction.None;
+        private MixedRealityInputAction manipulationAction = MixedRealityInputAction.None;
+        private MixedRealityInputAction selectAction = MixedRealityInputAction.None;
+#endif // MSFT_OPENXR_0_9_4_OR_NEWER && WINDOWS_UWP
+
         /// <inheritdoc />
         public override void Enable()
         {
@@ -50,8 +71,54 @@
                 return;
             }

+#if MSFT_OPENXR_0_9_4_OR_NEWER && WINDOWS_UWP
+            CreateGestureRecognizers();
+#endif // MSFT_OPENXR_0_9_4_OR_NEWER && WINDOWS_UWP
+
             base.Enable();
         }
+
+#if MSFT_OPENXR_0_9_4_OR_NEWER && WINDOWS_UWP
+        /// <inheritdoc />
+        public override void Initialize()
+        {
+            base.Initialize();
+
+            ReadProfile();
+        }
+
+        /// <inheritdoc />
+        public override void Update()
+        {
+            if (!IsEnabled)
+            {
+                return;
+            }
+
+            base.Update();
+
+            CheckForGestures();
+        }
+
+        /// <inheritdoc />
+        public override void Disable()
+        {
+            if (!IsEnabled)
+            {
+                return;
+            }
+
+            gestureRecognizer?.Stop();
+            gestureRecognizer?.Dispose();
+            gestureRecognizer = null;
+
+            navigationGestureRecognizer?.Stop();
+            navigationGestureRecognizer?.Dispose();
+            navigationGestureRecognizer = null;
+
+            base.Disable();
+        }
+#endif // MSFT_OPENXR_0_9_4_OR_NEWER && WINDOWS_UWP

         #region Controller Utilities

@@ -176,5 +243,252 @@
         }

         #endregion Controller Utilities
+
+        #region Gesture implementation
+
+#if MSFT_OPENXR_0_9_4_OR_NEWER && WINDOWS_UWP
+        private void ReadProfile()
+        {
+            if (InputSystemProfile.GesturesProfile != null)
+            {
+                MixedRealityGesturesProfile gestureProfile = InputSystemProfile.GesturesProfile;
+                gestureSettings = gestureProfile.ManipulationGestures;
+                navigationSettings = gestureProfile.NavigationGestures;
+                railsNavigationSettings = gestureProfile.RailsNavigationGestures;
+                useRailsNavigation = gestureProfile.UseRailsNavigation;
+                autoStartBehavior = gestureProfile.WindowsGestureAutoStart;
+
+                for (int i = 0; i < gestureProfile.Gestures.Length; i++)
+                {
+                    var gesture = gestureProfile.Gestures[i];
+
+                    switch (gesture.GestureType)
+                    {
+                        case GestureInputType.Hold:
+                            holdAction = gesture.Action;
+                            break;
+                        case GestureInputType.Manipulation:
+                            manipulationAction = gesture.Action;
+                            break;
+                        case GestureInputType.Navigation:
+                            navigationAction = gesture.Action;
+                            break;
+                        case GestureInputType.Select:
+                            selectAction = gesture.Action;
+                            break;
+                    }
+                }
+            }
+        }
+
+        private void CreateGestureRecognizers()
+        {
+            if (holdAction != MixedRealityInputAction.None ||
+                manipulationAction != MixedRealityInputAction.None ||
+                selectAction != MixedRealityInputAction.None)
+            {
+                if (gestureRecognizer == null)
+                {
+                    try
+                    {
+                        gestureRecognizer = new GestureRecognizer((GestureSettings)gestureSettings);
+
+                        if (autoStartBehavior == AutoStartBehavior.AutoStart)
+                        {
+                            gestureRecognizer.Start();
+                        }
+                    }
+                    catch (Exception ex)
+                    {
+                        UnityEngine.Debug.LogWarning($"Failed to create gesture recognizer. OS version might not support it. Exception: {ex}");
+                        gestureRecognizer = null;
+                        return;
+                    }
+                }
+            }
+
+            if (navigationAction != MixedRealityInputAction.None)
+            {
+                if (navigationGestureRecognizer == null)
+                {
+                    try
+                    {
+                        navigationGestureRecognizer = new GestureRecognizer((GestureSettings)(useRailsNavigation ? railsNavigationSettings : navigationSettings));
+
+                        if (autoStartBehavior == AutoStartBehavior.AutoStart)
+                        {
+                            navigationGestureRecognizer.Start();
+                        }
+                    }
+                    catch (Exception ex)
+                    {
+                        UnityEngine.Debug.LogWarning($"Failed to create gesture recognizer. OS version might not support it. Exception: {ex}");
+                        navigationGestureRecognizer = null;
+                        return;
+                    }
+                }
+            }
+        }
+
+        private void CheckForGestures()
+        {
+            if (gestureRecognizer != null)
+            {
+                while (gestureRecognizer.TryGetNextEvent(ref eventData))
+                {
+                    switch (eventData.EventType)
+                    {
+                        case GestureEventType.Tapped:
+                            if (selectAction != MixedRealityInputAction.None)
+                            {
+                                GenericXRSDKController controller = FindMatchingController(eventData.Handedness);
+                                if (controller != null)
+                                {
+                                    Service?.RaiseGestureCompleted(controller, selectAction);
+                                }
+                            }
+                            break;
+                        case GestureEventType.HoldStarted:
+                            if (holdAction != MixedRealityInputAction.None)
+                            {
+                                GenericXRSDKController controller = FindMatchingController(eventData.Handedness);
+                                if (controller != null)
+                                {
+                                    Service?.RaiseGestureStarted(controller, holdAction);
+                                }
+                            }
+                            break;
+                        case GestureEventType.HoldCompleted:
+                            if (holdAction != MixedRealityInputAction.None)
+                            {
+                                GenericXRSDKController controller = FindMatchingController(eventData.Handedness);
+                                if (controller != null)
+                                {
+                                    Service?.RaiseGestureCompleted(controller, holdAction);
+                                }
+                            }
+                            break;
+                        case GestureEventType.HoldCanceled:
+                            if (holdAction != MixedRealityInputAction.None)
+                            {
+                                GenericXRSDKController controller = FindMatchingController(eventData.Handedness);
+                                if (controller != null)
+                                {
+                                    Service?.RaiseGestureCanceled(controller, holdAction);
+                                }
+                            }
+                            break;
+                        case GestureEventType.ManipulationStarted:
+                            if (manipulationAction != MixedRealityInputAction.None)
+                            {
+                                GenericXRSDKController controller = FindMatchingController(eventData.Handedness);
+                                if (controller != null)
+                                {
+                                    Service?.RaiseGestureStarted(controller, manipulationAction);
+                                }
+                            }
+                            break;
+                        case GestureEventType.ManipulationUpdated:
+                            if (manipulationAction != MixedRealityInputAction.None)
+                            {
+                                GenericXRSDKController controller = FindMatchingController(eventData.Handedness);
+                                if (controller != null)
+                                {
+                                    Service?.RaiseGestureUpdated(controller, manipulationAction, eventData.ManipulationData.GetValueOrDefault().CumulativeTranslation);
+                                }
+                            }
+                            break;
+                        case GestureEventType.ManipulationCompleted:
+                            if (manipulationAction != MixedRealityInputAction.None)
+                            {
+                                GenericXRSDKController controller = FindMatchingController(eventData.Handedness);
+                                if (controller != null)
+                                {
+                                    Service?.RaiseGestureCompleted(controller, manipulationAction, eventData.ManipulationData.GetValueOrDefault().CumulativeTranslation);
+                                }
+                            }
+                            break;
+                        case GestureEventType.ManipulationCanceled:
+                            if (manipulationAction != MixedRealityInputAction.None)
+                            {
+                                GenericXRSDKController controller = FindMatchingController(eventData.Handedness);
+                                if (controller != null)
+                                {
+                                    Service?.RaiseGestureCanceled(controller, manipulationAction);
+                                }
+                            }
+                            break;
+                    }
+                }
+            }
+
+            if (navigationGestureRecognizer != null)
+            {
+                while (navigationGestureRecognizer.TryGetNextEvent(ref eventData))
+                {
+                    switch (eventData.EventType)
+                    {
+                        case GestureEventType.NavigationStarted:
+                            if (navigationAction != MixedRealityInputAction.None)
+                            {
+                                GenericXRSDKController controller = FindMatchingController(eventData.Handedness);
+                                if (controller != null)
+                                {
+                                    Service?.RaiseGestureStarted(controller, navigationAction);
+                                }
+                            }
+                            break;
+                        case GestureEventType.NavigationUpdated:
+                            if (navigationAction != MixedRealityInputAction.None)
+                            {
+                                GenericXRSDKController controller = FindMatchingController(eventData.Handedness);
+                                if (controller != null)
+                                {
+                                    Service?.RaiseGestureUpdated(controller, navigationAction, eventData.NavigationData.GetValueOrDefault().NormalizedOffset);
+                                }
+                            }
+                            break;
+                        case GestureEventType.NavigationCompleted:
+                            if (navigationAction != MixedRealityInputAction.None)
+                            {
+                                GenericXRSDKController controller = FindMatchingController(eventData.Handedness);
+                                if (controller != null)
+                                {
+                                    Service?.RaiseGestureCompleted(controller, navigationAction, eventData.NavigationData.GetValueOrDefault().NormalizedOffset);
+                                }
+                            }
+                            break;
+                        case GestureEventType.NavigationCanceled:
+                            if (navigationAction != MixedRealityInputAction.None)
+                            {
+                                GenericXRSDKController controller = FindMatchingController(eventData.Handedness);
+                                if (controller != null)
+                                {
+                                    Service?.RaiseGestureCanceled(controller, navigationAction);
+                                }
+                            }
+                            break;
+                    }
+                }
+            }
+        }
+
+        private GenericXRSDKController FindMatchingController(GestureHandedness gestureHandedness)
+        {
+            Utilities.Handedness handedness = gestureHandedness == GestureHandedness.Left ? Utilities.Handedness.Left : Utilities.Handedness.Right;
+
+            foreach (GenericXRSDKController controller in ActiveControllers.Values)
+            {
+                if (controller.ControllerHandedness == handedness)
+                {
+                    return controller;
+                }
+            }
+
+            return null;
+        }
+#endif // MSFT_OPENXR_0_9_4_OR_NEWER && WINDOWS_UWP
+
+        #endregion Gesture implementation
     }
 }


update

update

--- CSharpGL/Scene/GUI/GLControls/CtrlButton/CtrlButtonModel.cs
+++ CSharpGL/Scene/GUI/GLControls/CtrlButton/CtrlButtonModel.cs
@@ -27,12 +27,12 @@
         private const float scaleFactor = 0.95f;
         private static readonly vec2[] positions = new vec2[]
         {
-            // button up.
+            // button pressed up.
             sixPositions[0], sixPositions[1],
             sixPositions[2], sixPositions[3],
             sixPositions[4], sixPositions[5],

-            // button down.
+            // button pressed down.
             sixPositions[0] * scaleFactor, sixPositions[1] * scaleFactor,
             sixPositions[2] * scaleFactor, sixPositions[3] * scaleFactor,
             sixPositions[4] * scaleFactor, sixPositions[5] * scaleFactor,


Readded parameter file option aliases similar as in commandline.

--- Duplicati/Server/Program.cs
+++ Duplicati/Server/Program.cs
@@ -214,17 +214,17 @@
             // Check if a parameters-file was provided. Skip if help was already specified
             if (!commandlineOptions.ContainsKey("help"))
             {
-                string filename = null;
-                if (commandlineOptions.ContainsKey("parameters-file"))
-                {
-                    filename = commandlineOptions["parameters-file"];
-                    commandlineOptions.Remove("parameters-file");
-                }
-
-                if (!string.IsNullOrEmpty(filename))
-                {
-                    // Check the parameters-file and add provided arguments (overriding commandline arguments)
-                    ReadOptionsFromFile(filename, ref filter, args, commandlineOptions);
+                // try and parse all parameter file aliases
+                foreach (string parameterOption in new[] { "parameters-file", "parameters-file", "parameterfile" })
+                {
+                    if (commandlineOptions.ContainsKey(parameterOption) && !string.IsNullOrEmpty(commandlineOptions[parameterOption]))
+                    {
+                        string filename = commandlineOptions[parameterOption];
+                        commandlineOptions.Remove(parameterOption);
+                        if (!ReadOptionsFromFile(filename, ref filter, args, commandlineOptions))
+                            return 100;
+                        break;
+                    }
                 }
             }


improved parsing error reporting

improved parsing error reporting

--- EAMapping/EAMappingAddin.cs
+++ EAMapping/EAMappingAddin.cs
@@ -276,8 +276,21 @@

     private void importCopybook(string source, TSF_EA.Package package) {
       // parse copybook into OO data-representation
-      var mapped = new Mapper().Parse(source);
-
+      var mapped = new Mapper();
+      try {
+        mapped.Parse(source);
+      } catch( ParseException e ) {
+        // recurse down the Exception tree, to reach the most specific one
+        this.log("!!! IMPORT FAILED");
+        do {
+          foreach(var line in e.Message.Split('\n') ) {
+            this.log(line);
+          }
+          e = e.InnerException as ParseException;
+        } while(e != null);
+        return;
+      }
+
       // import mapped OO data-representation

       // prepare cache


Fix clickmaps (#2967)

Fix clickmaps (#2967)

Co-authored-by: Metal Gear Sloth <5a6002283f4d0ed17bed24ee1f9d131baf85176c@gmail.com>

--- Content.Client/GameObjects/Components/ClickableComponent.cs
+++ Content.Client/GameObjects/Components/ClickableComponent.cs
@@ -87,6 +87,8 @@
             {
                 foreach (var layer in sprite.AllLayers)
                 {
+                    if (!layer.Visible) continue;
+
                     if (layer.Texture != null)
                     {
                         if (_clickMapManager.IsOccluding(layer.Texture,


allow flag to set datetimehandling utc/local

--- EDILibrary/EdiJsonMapper.cs
+++ EDILibrary/EdiJsonMapper.cs
@@ -80,7 +80,7 @@
             result.Receiver = edi_info.Empfänger.ID;
             return result;
         }
-        public async Task<string> CreateFromJson(string jsonInput, string pid, string formatPackage = null)
+        public async Task<string> CreateFromJson(string jsonInput, string pid, string formatPackage = null, bool convertFromUTC = false)
         {
             string format = "ERROR";
             //format is derived from the pid
@@ -143,7 +143,7 @@
                 }
                 //maskArray.Merge(((step as JObject)?.Property("fields").Value as JObject).Properties(), new JsonMergeSettings() { MergeArrayHandling = MergeArrayHandling.Union });
             }
-            var outputJson = CreateMsgJSON(inputJson, mappings, maskArray, out var subParent);
+            var outputJson = CreateMsgJSON(inputJson, mappings, maskArray, out var subParent, convertFromUTC);
             IEdiObject result = IEdiObject.CreateFromJSON(JsonConvert.SerializeObject(outputJson));
             //apply scripts
             return await new MappingHelper().ExecuteMappings(result, new EDIFileInfo { Format = format, Version = version }, new List<string>(), _loader, false);
@@ -265,7 +265,7 @@
             }
             return false;
         }
-        protected dynamic CreateMsgJSON(JObject input, JArray mapping, JArray mask, out bool createInParent)
+        protected dynamic CreateMsgJSON(JObject input, JArray mapping, JArray mask, out bool createInParent, bool convertFromUTC = false)
         {

             createInParent = false;
@@ -302,7 +302,7 @@
                             // war früher ein JObject, daher hier die Ausnahme abfangen
                             if (prop.Value.GetType() == typeof(JObject))
                                 continue;
-                            var newSub = CreateMsgJSON((prop.Value as JArray)[0] as JObject, newArray, mask, out var subParent);
+                            var newSub = CreateMsgJSON((prop.Value as JArray)[0] as JObject, newArray, mask, out var subParent, convertFromUTC);
                             foreach (KeyValuePair<string, object> subProp in newSub as IDictionary<string, object>)
                                 (returnObject as IDictionary<string, object>).Add(subProp.Key, subProp.Value);
                             continue;
@@ -314,7 +314,7 @@
                             {
                                 string format = subObj.SelectToken("_meta.format").Value<string>();
                                 //format date
-                                (returnObject as IDictionary<string, object>).Add(propVal.Value<string>(), new ScriptHelper { useLocalTime = false }.FormatDate(prop.Value.Value<string>(), format));
+                                (returnObject as IDictionary<string, object>).Add(propVal.Value<string>(), new ScriptHelper { useLocalTime = convertFromUTC }.FormatDate(prop.Value.Value<string>(), format));
                             }
                             else
                             {
@@ -338,7 +338,7 @@
                                     {
                                         if (sub is JObject)
                                         {
-                                            var newSub = CreateMsgJSON(sub as JObject, newArray, mask, out var subParent);
+                                            var newSub = CreateMsgJSON(sub as JObject, newArray, mask, out var subParent, convertFromUTC);
                                             if (!subParent)
                                                 ((returnObject as IDictionary<string, object>)[newPropName] as List<dynamic>).Add(newSub);
                                             else
@@ -355,7 +355,7 @@
                                 }
                                 else
                                 {
-                                    var newSub = CreateMsgJSON(prop.Value as JObject, newArray, mask, out var subParent);
+                                    var newSub = CreateMsgJSON(prop.Value as JObject, newArray, mask, out var subParent, convertFromUTC);
                                     if (!subParent)
                                         (returnObject as IDictionary<string, object>).Add(newPropName, newSub);
                                     else


allow specific StringComparison

allow specific StringComparison

--- src/StringExtensions.cs
+++ src/StringExtensions.cs
@@ -20,9 +20,9 @@
 		/// <param name="startIndex">The search starting position.</param>
 		/// <returns>Reports the zero-based index of the <i>end</i> of the first occurrence of the specified <paramref name="find" /> string in the current <see cref="System.String" /> object.</returns>
 		/// <exception cref="ArgumentOutOfRangeException"><paramref name="startIndex" /> is less than 0 (zero) or greater than the length of this string.</exception>
-		public static int IndexOfEnd(this string value, string find, int? startIndex = null)
+		public static int IndexOfEnd(this string value, string find, int? startIndex = null, StringComparison compare = StringComparison.Ordinal)
 		{
-			var pos = value.IndexOf(find, startIndex ?? 0, StringComparison.Ordinal);
+			var pos = value.IndexOf(find, startIndex ?? 0, compare);
 			if (pos > -1)
 				pos += find.Length;
 			return pos;
@@ -35,10 +35,10 @@
 		/// <param name="find">The string to seek.</param>
 		/// <returns>Reports the zero-based indexes of all the occurrences of the specified <paramref name="find" /> string in the current <see cref="System.String" /> object.</returns>
 		/// <exception cref="ArgumentNullException"><paramref name="find" /> is <c>null</c>.</exception>
-		public static IEnumerable<int> AllIndexesOf(this string value, string find)
+		public static IEnumerable<int> AllIndexesOf(this string value, string find, StringComparison compare = StringComparison.Ordinal)
 		{
 			var pos = 0;
-			while ((pos = value.IndexOf(find, pos, StringComparison.Ordinal)) > -1)
+			while ((pos = value.IndexOf(find, pos, compare)) > -1)
 			{
 				yield return pos;
 				pos += find.Length;
@@ -53,12 +53,12 @@
 		/// <returns>Reports the zero-based indexes of all the occurrences of the specified <paramref name="find" /> strings in the current <see cref="System.String" /> object.</returns>
 		/// <exception cref="ArgumentNullException"><paramref name="find" /> contains a <c>null</c> value.</exception>
 		/// <remarks>Searches through <paramref name="value" /> for each <paramref name="find" /> in parallel, for increased performance. Therefore, note that the order in which the indexes are returned is not deterministic. See <see cref="AllSortedIndexesOf" />.</remarks>
-		private static IEnumerable<KeyValuePair<string, int>> AllIndexesOf(this string value, IEnumerable<string> find)
+		private static IEnumerable<KeyValuePair<string, int>> AllIndexesOf(this string value, IEnumerable<string> find, StringComparison compare = StringComparison.Ordinal)
 		{
 			var results = new ConcurrentBag<KeyValuePair<string, int>>();
 			Parallel.ForEach(find, search =>
 								   {
-									   var r = AllIndexesOf(value, search).Select(pos => new KeyValuePair<string, int>(search, pos));
+									   var r = AllIndexesOf(value, search, compare).Select(pos => new KeyValuePair<string, int>(search, pos));
 									   foreach (var kvp in r) results.Add(kvp); // unable to yield return from here, so add results concurrently
 								   });
 			return results;
@@ -82,9 +82,9 @@
 		/// <param name="find">The strings to seek.</param>
 		/// <returns>Reports the zero-based indexes of all the occurrences of the specified <paramref name="find" /> strings in the current <see cref="System.String" /> object, in the order in which they appear.</returns>
 		/// <exception cref="ArgumentNullException"><paramref name="find" /> contains a <c>null</c> value.</exception>
-		public static IOrderedEnumerable<KeyValuePair<string, int>> AllSortedIndexesOf(this string value, IEnumerable<string> find)
+		public static IOrderedEnumerable<KeyValuePair<string, int>> AllSortedIndexesOf(this string value, IEnumerable<string> find, StringComparison compare = StringComparison.Ordinal)
 		{
-			var indexes = AllIndexesOf(value, find);
+			var indexes = AllIndexesOf(value, find, compare);
 			return indexes.OrderBy(i => i.Value);
 		}
 		#endregion
@@ -97,9 +97,9 @@
 		/// <param name="find">The string to seek.</param>
 		/// <returns>Reports the number of times the specified <paramref name="find" /> string occurs in the current <see cref="System.String" /> object.</returns>
 		/// <exception cref="ArgumentNullException"><paramref name="find" /> is <c>null</c>.</exception>
-		public static int CountOccurrences(this string value, string find)
+		public static int CountOccurrences(this string value, string find, StringComparison compare = StringComparison.Ordinal)
 		{
-			return value.AllIndexesOf(find).Count();
+			return value.AllIndexesOf(find, compare).Count();
 		}

 		/// <summary>
@@ -109,9 +109,9 @@
 		/// <param name="find">The string to seek.</param>
 		/// <returns>Retrieves the text that occurs in the current <see cref="System.String" /> object before the first occurrence of <paramref name="find" />, or an empty string if there are no occurrences.</returns>
 		/// <exception cref="ArgumentNullException"><paramref name="find" /> is <c>null</c>.</exception>
-		public static string TextBefore(this string value, string find)
+		public static string TextBefore(this string value, string find, StringComparison compare = StringComparison.Ordinal)
 		{
-			var pos = value.IndexOf(find, StringComparison.Ordinal);
+			var pos = value.IndexOf(find, compare);
 			if (pos == -1)
 				pos = 0;
 			return value.Substring(0, pos);
@@ -124,9 +124,9 @@
 		/// <param name="find">The string to seek.</param>
 		/// <returns>Retrieves the text that occurs in the current <see cref="System.String" /> object after the first occurrence of <paramref name="find" />, or an empty string if there are no occurrences.</returns>
 		/// <exception cref="ArgumentNullException"><paramref name="find" /> is <c>null</c>.</exception>
-		public static string TextAfter(this string value, string find)
+		public static string TextAfter(this string value, string find, StringComparison compare = StringComparison.Ordinal)
 		{
-			var pos = value.IndexOfEnd(find);
+			var pos = value.IndexOfEnd(find, null, compare);
 			if (pos == -1)
 				pos = value.Length;
 			return value.Substring(pos);
@@ -140,9 +140,9 @@
 		/// <param name="end">The next string to seek.</param>
 		/// <returns>Retrieves the text that occurs in the current <see cref="System.String" /> object after the first occurrence of <paramref name="start" /> and before the first subsequent occurrence of <paramref name="end" />, or an empty string if either are not found.</returns>
 		/// <exception cref="ArgumentNullException"><paramref name="start" /> or <paramref name="end" /> is <c>null</c>.</exception>
-		public static string TextBetween(this string value, string start, string end)
+		public static string TextBetween(this string value, string start, string end, StringComparison compare = StringComparison.Ordinal)
 		{
-			return value.TextAfter(start).TextBefore(end);
+			return value.TextAfter(start, compare).TextBefore(end, compare);
 		}

 		/// <summary>
@@ -153,10 +153,10 @@
 		/// <param name="end">The end string to seek.</param>
 		/// <returns>Retrieves the text that occurs in the current <see cref="System.String" /> object after each occurrence of <paramref name="start" /> and before the first subsequent occurrence of <paramref name="end" />, or an empty enumerable.</returns>
 		/// <exception cref="ArgumentNullException"><paramref name="start" /> or <paramref name="end" /> is <c>null</c>.</exception>
-		public static IEnumerable<string> AllTextBetween(this string value, string start, string end)
+		public static IEnumerable<string> AllTextBetween(this string value, string start, string end, StringComparison compare = StringComparison.Ordinal)
 		{
 			// get all indexes of the start and end tokens, sorted in order
-			var results = value.AllSortedIndexesOf(new[] { start, end });
+			var results = value.AllSortedIndexesOf(new[] { start, end }, compare);
 			var nextIsStart = true; // first we want a start token
 			var startpos = -1;
 			// for each occurrence


fix an annoying crash caused by an obtuse binding

--- src/Summa/WebKitView.cs
+++ src/Summa/WebKitView.cs
@@ -83,7 +83,7 @@
         private void OnLinkClicked(object o, NavigationRequestedArgs args) {
             Gnome.Url.Show(args.Request.Uri);

-            return;
+            args.RetVal = WebKit.NavigationResponse.Ignore;
         }

         public void Render(string data) {


Missing Types array on Places Candidate response

--- GoogleApi/Entities/Places/Search/Find/Response/Candidate.cs
+++ GoogleApi/Entities/Places/Search/Find/Response/Candidate.cs
@@ -100,5 +100,11 @@
         /// </summary>
         [JsonProperty("photos")]
         public virtual IEnumerable<Photo> Photos { get; set; }
+
+        /// <summary>
+        /// An array of types for this place
+        /// </summary>
+        [JsonProperty("types")]
+        public IEnumerable<PlaceLocationType> Types { get; set; }
     }
 }


Added a try catch

Added a try catch

--- HeroesParserData/ViewModels/Heroes/HeroesUsableViewModel.cs
+++ HeroesParserData/ViewModels/Heroes/HeroesUsableViewModel.cs
@@ -5,8 +5,8 @@
 using System;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
-using System.Threading.Tasks;
 using System.Windows.Input;
+using System.Windows.Markup;

 namespace HeroesParserData.ViewModels.Heroes
 {
@@ -93,27 +93,34 @@
             int heroCount = 0;
             int row = 0;

-            while (heroCount < listOfHeroes.Count)
-            {
-                HeroesUsable heroesUsable = new HeroesUsable();
-
-                for (int column = 0; column < 15; column++)
+            try
+            {
+                while (heroCount < listOfHeroes.Count)
                 {
-                    if (heroCount < listOfHeroes.Count)
+                    HeroesUsable heroesUsable = new HeroesUsable();
+
+                    for (int column = 0; column < 15; column++)
                     {
-                        heroesUsable.HeroName[column] = listOfHeroes[heroCount];
-                        heroesUsable.HeroPortrait[column] = HeroesInfo.GetHeroPortrait(listOfHeroes[heroCount]);
-                        heroesUsable.IsXOut.Add(true);
-                        HeroesGridLocation.Add(listOfHeroes[heroCount], new Tuple<int, int>(row, column));
+                        if (heroCount < listOfHeroes.Count)
+                        {
+                            heroesUsable.HeroName[column] = listOfHeroes[heroCount];
+                            heroesUsable.HeroPortrait[column] = HeroesInfo.GetHeroPortrait(listOfHeroes[heroCount]);
+                            heroesUsable.IsXOut.Add(true);
+                            HeroesGridLocation.Add(listOfHeroes[heroCount], new Tuple<int, int>(row, column));
+                        }
+                        else
+                            heroesUsable.IsXOut.Add(false);
+
+                        heroCount++;
                     }
-                    else
-                        heroesUsable.IsXOut.Add(false);
-
-                    heroCount++;
-                }
-
-                Heroes.Add(heroesUsable);
-                row++;
+
+                    Heroes.Add(heroesUsable);
+                    row++;
+                }
+            }
+            catch (Exception ex) when (ex is XamlParseException || ex is NotSupportedException)
+            {
+                ExceptionLog.Log(LogLevel.Error, ex, $"{nameof(LoadHeroPortraits)}: Failed to load");
             }
         }


Added test for collection profile href

Added test for collection profile href

--- Hyperfriendly.WebApi.Tests/formatting_collection_profile.cs
+++ Hyperfriendly.WebApi.Tests/formatting_collection_profile.cs
@@ -7,6 +7,16 @@
     public class formatting_collection_profile
     {
         private static readonly HyperfriendlyJsonMediaTypeFormatter _mediaFormatter = new HyperfriendlyJsonMediaTypeFormatter { Indent = true };
+
+        [Fact]
+        public void contains_profile()
+        {
+            var collectionResource = new CollectionResource<FooResource>();
+
+            var jToken = _mediaFormatter.Format(collectionResource);
+
+            jToken.SelectToken("_links.profile.href").Value<string>().ShouldEqual(Profiles.Collection);
+        }

         [Fact]
         public void items_element_is_json_array()


Log is renamed AbnakiLog

Log is renamed AbnakiLog

--- Library/Software/WpfApplication/Ultimate/MainWindow.xaml.cs
+++ Library/Software/WpfApplication/Ultimate/MainWindow.xaml.cs
@@ -79,7 +79,7 @@
             }
             catch (Exception ex)
             {
-                Log.Exception(ex);
+                AbnakiLog.Exception(ex);
             }
         }

@@ -146,7 +146,7 @@
                 }
                 else
                 {
-                    Log.Comment(GetType().Name + " disregarded undisplayable bounds " + pref.Bounds);
+                    AbnakiLog.Comment(GetType().Name + " disregarded undisplayable bounds " + pref.Bounds);
                 }
             }


Annotations

Annotations

--- Main/src/Collections/EnumerableExtensions.AggregateFuncs.cs
+++ Main/src/Collections/EnumerableExtensions.AggregateFuncs.cs
@@ -18,6 +18,7 @@
 		/// <param name="source">A sequence of values to determine the minimum value of.</param>
 		/// <param name="selector">A transform function to apply to each element.</param>
 		/// <returns>The item with minimum value in the sequence.</returns>
+		[Pure]
 		public static TSource Min<TSource, TValue>(
 			[NotNull] this IEnumerable<TSource> source,
 			[NotNull, InstantHandle] Func<TSource, TValue> selector)
@@ -86,6 +87,7 @@
 		/// <param name="source">A sequence of values to determine the maximum value of.</param>
 		/// <param name="selector">A transform function to apply to each element.</param>
 		/// <returns>The item with maximum value in the sequence.</returns>
+		[Pure]
 		public static TSource Max<TSource, TValue>(
 			[NotNull] this IEnumerable<TSource> source,
 			[NotNull, InstantHandle] Func<TSource, TValue> selector)


Tided up code, fixed remembering of since setting, changed default since for favorites to "Show All"

--- yafsrc/YetAnotherForum.NET/controls/MyTopicsList.ascx.cs
+++ yafsrc/YetAnotherForum.NET/controls/MyTopicsList.ascx.cs
@@ -20,251 +20,326 @@

 namespace YAF.Controls
 {
-    using System;
-    using System.Data;
-    using System.Web.UI;
-    using System.Web.UI.HtmlControls;
-    using System.Web.UI.WebControls;
-    using YAF.Classes;
-    using YAF.Classes.Core;
-    using YAF.Classes.Data;
-    using YAF.Classes.UI;
-    using YAF.Classes.Utils;
-    using YAF.Utilities;
-
-    ///
-    /// Summary description for buddies.
-    ///
-    public partial class MyTopicsList : BaseUserControl
-    {
-        ///
-        /// The _last forum name.
-        ///
-        protected string _lastForumName = string.Empty;
-
-        ///
-        /// the control mode.
-        ///
-        private int _controlMode;
-
-        ///
-        /// Determines what is th current mode of the control.
-        ///
-        public int Mode
-        {
-            get
-            {
-                return this._controlMode;
-            }
-            set
-            {
-                this._controlMode = value;
-            }
-
-        }
-        ///
-        /// The page_ load.
-        ///
-        /// <param name="sender">
-        /// The sender.
-        /// </param>
-        /// <param name="e">
-        /// The e.
-        /// </param>
-        protected void Page_Load(object sender, EventArgs e)
-        {
-            if (PageContext.BoardSettings.ShowRSSLink)
-            {
-                if (this.Mode == 1)
-                {
-                    this.RssFeed.NavigateUrl = YafBuildLink.GetLinkNotEscaped(ForumPages.rsstopic, "pg=active");
-                }
-                else
-                {
-                    this.RssFeed.NavigateUrl = YafBuildLink.GetLinkNotEscaped(ForumPages.rsstopic, "pg=favorite");
-                }
-                this.RssFeed.Text = PageContext.Localization.GetText("RSSFEED");
-                this.RssFeed.Visible = true;
-            }
-            else
-            {
-                this.RssFeed.Visible = false;
-            }
-            if (!IsPostBack)
-            {
-                this.Since.Items.Add(new ListItem(PageContext.Localization.GetTextFormatted("last_visit", YafServices.DateTime.FormatDateTime(Mession.LastVisit)), "0"));
-                this.Since.Items.Add(new ListItem(PageContext.Localization.GetText("last_hour"), "-1"));
-                this.Since.Items.Add(new ListItem(PageContext.Localization.GetText("last_two_hours"), "-2"));
-                this.Since.Items.Add(new ListItem(PageContext.Localization.GetText("last_day"), "1"));
-                this.Since.Items.Add(new ListItem(PageContext.Localization.GetText("last_two_days"), "2"));
-                this.Since.Items.Add(new ListItem(PageContext.Localization.GetText("last_week"), "7"));
-                this.Since.Items.Add(new ListItem(PageContext.Localization.GetText("last_two_weeks"), "14"));
-                this.Since.Items.Add(new ListItem(PageContext.Localization.GetText("last_month"), "31"));
-
-                if (this.Mode == 1)
-                {
-                    if (Mession.ActiveTopicSince.HasValue)
-                    {
-                        ListItem activeTopicItem = this.Since.Items.FindByValue(Mession.ActiveTopicSince.Value.ToString());
-                        if (activeTopicItem != null)
-                        {
-                            activeTopicItem.Selected = true;
-                        }
-                    }
-                    else
-                    {
-                        this.Since.SelectedIndex = 0;
-                    }
-                }
-                else
-                {
-                    this.Since.Items.Add(new ListItem(PageContext.Localization.GetText("show_all"), "365"));
-                    if (Mession.FavoriteTopicSince.HasValue)
-                    {
-                        ListItem favoriteTopicItem = this.Since.Items.FindByValue(Mession.FavoriteTopicSince.Value.ToString());
-                        if (favoriteTopicItem != null)
-                        {
-                            favoriteTopicItem.Selected = true;
-                        }
-                    }
-                    else
-                    {
-                        this.Since.SelectedIndex = 0;
-                    }
-                }
-            }
-            BindData();
-        }
-
-        ///
-        /// The since_ selected index changed.
-        ///
-        /// <param name="sender">
-        /// The sender.
-        /// </param>
-        /// <param name="e">
-        /// The e.
-        /// </param>
-        protected void Since_SelectedIndexChanged(object sender, EventArgs e)
-        {
-            // Set the controls' pager index to 0.
-            PagerTop.CurrentPageIndex = 0;
-            if (Mode == 1)
-            {
-                Mession.ActiveTopicSince = Convert.ToInt32(this.Since.SelectedValue);
-            }
-            else
-            {
-                Mession.FavoriteTopicSince = Convert.ToInt32(this.Since.SelectedValue);
-            }
-            BindData();
-        }
-
-        ///
-        /// The print forum name.
-        ///
-        /// <param name="row">
-        /// The row.
-        /// </param>
-        /// <returns>
-        /// The print forum name.
-        /// </returns>
-        protected string PrintForumName(DataRowView row)
-        {
-            var forumName = (string)row["ForumName"];
-            string html = string.Empty;
-            if (forumName != this._lastForumName)
-            {
-                html = String.Format(
-                  @"
{0}
",
-                  forumName,
-                  YafBuildLink.GetLink(ForumPages.topics, "f={0}", row["ForumID"]));
-                this._lastForumName = forumName;
-            }
-
-            return html;
-        }
-
-        ///
-        /// The pager_ page change.
-        ///
-        /// <param name="sender">
-        /// The sender.
-        /// </param>
-        /// <param name="e">
-        /// The e.
-        /// </param>
-        protected void Pager_PageChange(object sender, EventArgs e)
-        {
-            BindData();
-        }
-
-        ///
-        /// The bind data.
-        ///
-        public void BindData()
-        {
-            DateTime sinceDate = DateTime.Now;
-            int sinceValue = 0;
-
-            if (this.Since.SelectedItem != null)
-            {
-                sinceValue = int.Parse(this.Since.SelectedItem.Value);
-                sinceDate = DateTime.Now;
-                if (sinceValue == 365)
-                {
-                    sinceDate = (DateTime)(System.Data.SqlTypes.SqlDateTime.MinValue);
-                }
-
-                else if (sinceValue > 0)
-                {
-                    sinceDate = DateTime.Now - TimeSpan.FromDays(sinceValue);
-                }
-                else if (sinceValue < 0)
-                {
-                    sinceDate = DateTime.Now + TimeSpan.FromHours(sinceValue);
-                }
-            }
-
-            if (sinceValue == 0)
-            {
-                sinceDate = Mession.LastVisit;
-            }
-
-            var pds = new PagedDataSource();
-            pds.AllowPaging = true;
-
-            object categoryIDObject = null;
-
-            if (PageContext.Settings.CategoryID != 0)
-            {
-                categoryIDObject = PageContext.Settings.CategoryID;
-            }
-
-            DataTable topicList;
-            if (this.Mode == 1)
-            {
-                topicList = DB.topic_active(PageContext.PageBoardID, PageContext.PageUserID, sinceDate, categoryIDObject, PageContext.BoardSettings.UseStyledNicks);
-            }
-            else
-            {
-                topicList = YafServices.FavoriteTopic.FavoriteTopicDetails(sinceDate);
-            }
-            if (PageContext.BoardSettings.UseStyledNicks)
-            {
-                new StyleTransform(PageContext.Theme).DecodeStyleByTable(ref topicList, true, "LastUserStyle", "StarterStyle");
-            }
-
-            DataView dv = topicList.DefaultView;
-            pds.DataSource = dv;
-            this.PagerTop.Count = dv.Count;
-            this.PagerTop.PageSize = 15;
-            pds.PageSize = this.PagerTop.PageSize;
-
-            pds.CurrentPageIndex = this.PagerTop.CurrentPageIndex;
-            this.TopicList.DataSource = pds;
-
-            DataBind();
-        }
-    }
+	using System;
+	using System.Data;
+	using System.Web.UI.WebControls;
+	using YAF.Classes;
+	using YAF.Classes.Core;
+	using YAF.Classes.Data;
+	using YAF.Classes.Utils;
+
+	///
+	/// Summary description for buddies.
+	///
+	public partial class MyTopicsList : BaseUserControl
+	{
+		/* Data Fields */
+		#region Control Data
+
+		///
+		/// The _last forum name.
+		///
+		private string _lastForumName = string.Empty;
+
+		///
+		/// the control mode.
+		///
+		private int _controlMode;
+
+		#endregion
+
+
+		/* Properties */
+		#region Control Mode
+
+		///
+		/// Determines what is th current mode of the control.
+		///
+		public int Mode
+		{
+			get
+			{
+				return this._controlMode;
+			}
+			set
+			{
+				this._controlMode = value;
+			}
+
+		}
+
+		#endregion
+
+
+		/* Event Handlers */
+		#region Page Events
+
+		///
+		/// The page_ load.
+		///
+		/// <param name="sender">
+		/// The sender.
+		/// </param>
+		/// <param name="e">
+		/// The e.
+		/// </param>
+		protected void Page_Load(object sender, EventArgs e)
+		{
+			// RSS link setup
+			if (PageContext.BoardSettings.ShowRSSLink)
+			{
+				if (this.Mode == 1)
+				{
+					// for active topics list
+					this.RssFeed.NavigateUrl = YafBuildLink.GetLinkNotEscaped(ForumPages.rsstopic, "pg=active");
+				}
+				else
+				{
+					// for favorites list
+					this.RssFeed.NavigateUrl = YafBuildLink.GetLinkNotEscaped(ForumPages.rsstopic, "pg=favorite");
+				}
+				this.RssFeed.Text = PageContext.Localization.GetText("RSSFEED");
+				this.RssFeed.Visible = true;
+			}
+			else
+			{
+				// hide RSS link if board has them turned off
+				this.RssFeed.Visible = false;
+			}
+
+			if (!IsPostBack)
+			{
+				InitSinceDropdown();
+
+				int? previousSince = null;
+
+				if (this.Mode == 1)
+				{
+					// active topics mode
+					previousSince = Mession.ActiveTopicSince;
+
+					// by default select "Last visited..." for active discussions
+					this.Since.SelectedIndex = 0;
+				}
+				else
+				{
+					// favorites mode
+					previousSince = Mession.FavoriteTopicSince;
+
+					// add "Show All" option
+					this.Since.Items.Add(new ListItem(PageContext.Localization.GetText("show_all"), "9999"));
+
+					// by default select "Show All" for favorites topics
+					this.Since.SelectedIndex = this.Since.Items.Count - 1;
+				}
+
+				// has user selected any "since" value before we can remember now?
+				if (previousSince.HasValue)
+				{
+					// look for value previously selected
+					ListItem sinceItem = this.Since.Items.FindByValue(previousSince.Value.ToString());
+					// and select it if found
+					if (sinceItem != null) this.Since.SelectedIndex = this.Since.Items.IndexOf(sinceItem);
+				}
+			}
+
+			BindData();
+		}
+
+		#endregion
+
+		#region Control Events
+
+		///
+		/// The since_ selected index changed.
+		///
+		/// <param name="sender">
+		/// The sender.
+		/// </param>
+		/// <param name="e">
+		/// The e.
+		/// </param>
+		protected void Since_SelectedIndexChanged(object sender, EventArgs e)
+		{
+			// Set the controls' pager index to 0.
+			PagerTop.CurrentPageIndex = 0;
+
+			// save since option to rememver it next time
+			if (Mode == 1)
+			{
+				// for active topics
+				Mession.ActiveTopicSince = Convert.ToInt32(this.Since.SelectedValue);
+			}
+			else
+			{
+				// for favorites
+				Mession.FavoriteTopicSince = Convert.ToInt32(this.Since.SelectedValue);
+			}
+
+			// re-bind data
+			BindData();
+		}
+
+		///
+		/// The pager_ page change.
+		///
+		/// <param name="sender">
+		/// The sender.
+		/// </param>
+		/// <param name="e">
+		/// The e.
+		/// </param>
+		protected void Pager_PageChange(object sender, EventArgs e)
+		{
+			BindData();
+		}
+
+		#endregion
+
+
+		/* Methods */
+		#region Data Binding
+
+		///
+		/// The bind data.
+		///
+		public void BindData()
+		{
+			// default since date is now
+			DateTime sinceDate = DateTime.Now;
+			// default since option is "since last visit"
+			int sinceValue = 0;
+
+			// is any "since"option selected
+			if (this.Since.SelectedItem != null)
+			{
+				// get selected value
+				sinceValue = int.Parse(this.Since.SelectedItem.Value);
+				//sinceDate = DateTime.Now;	// no need to do it again (look above)
+
+				// decrypt selected option
+				if (sinceValue == 9999)		// all
+				{
+					// get all, from the beginning
+					sinceDate = (DateTime)(System.Data.SqlTypes.SqlDateTime.MinValue);
+				}
+				else if (sinceValue > 0)	// days
+				{
+					// get posts newer then defined number of days
+					sinceDate = DateTime.Now - TimeSpan.FromDays(sinceValue);
+				}
+				else if (sinceValue < 0)	// hours
+				{
+					// get posts newer then defined number of hours
+					sinceDate = DateTime.Now + TimeSpan.FromHours(sinceValue);
+				}
+			}
+
+			// we want to filter topics since last visit
+			if (sinceValue == 0) sinceDate = Mession.LastVisit;
+
+			// we want to page results
+			var pds = new PagedDataSource();
+			pds.AllowPaging = true;
+
+			// filter by category
+			object categoryIDObject = null;
+			// is category set?
+			if (PageContext.Settings.CategoryID != 0)
+			{
+				categoryIDObject = PageContext.Settings.CategoryID;
+			}
+
+			// we'll hold topics in this table
+			DataTable topicList;
+			// now depending on mode fill the table
+			if (this.Mode == 1)
+			{
+				// we are getting active topics
+				topicList = DB.topic_active(
+					PageContext.PageBoardID,
+					PageContext.PageUserID,
+					sinceDate,
+					categoryIDObject,
+					PageContext.BoardSettings.UseStyledNicks
+					);
+			}
+			else
+			{
+				// we are getting favotes
+				topicList = YafServices.FavoriteTopic.FavoriteTopicDetails(sinceDate);
+			}
+
+			// styled nicks
+			if (PageContext.BoardSettings.UseStyledNicks)
+			{
+				new StyleTransform(PageContext.Theme).DecodeStyleByTable(ref topicList, true, "LastUserStyle", "StarterStyle");
+			}
+
+			// let's page the results
+			DataView dv = topicList.DefaultView;
+			pds.DataSource = dv;
+			this.PagerTop.Count = dv.Count;
+			/// TODO : page size definable?
+			this.PagerTop.PageSize = 15;
+			pds.PageSize = this.PagerTop.PageSize;
+			pds.CurrentPageIndex = this.PagerTop.CurrentPageIndex;
+
+			// set datasource of repeater
+			this.TopicList.DataSource = pds;
+
+			// data bind controls
+			DataBind();
+		}
+
+		///
+		/// Initializes dropdown with options to filter results by date.
+		///
+		protected void InitSinceDropdown()
+		{
+			// value 0, for since last visted
+			this.Since.Items.Add(new ListItem(PageContext.Localization.GetTextFormatted("last_visit", YafServices.DateTime.FormatDateTime(Mession.LastVisit)), "0"));
+			// negative values for hours backward
+			this.Since.Items.Add(new ListItem(PageContext.Localization.GetText("last_hour"), "-1"));
+			this.Since.Items.Add(new ListItem(PageContext.Localization.GetText("last_two_hours"), "-2"));
+			// positive values for days backward
+			this.Since.Items.Add(new ListItem(PageContext.Localization.GetText("last_day"), "1"));
+			this.Since.Items.Add(new ListItem(PageContext.Localization.GetText("last_two_days"), "2"));
+			this.Since.Items.Add(new ListItem(PageContext.Localization.GetText("last_week"), "7"));
+			this.Since.Items.Add(new ListItem(PageContext.Localization.GetText("last_two_weeks"), "14"));
+			this.Since.Items.Add(new ListItem(PageContext.Localization.GetText("last_month"), "31"));
+		}
+
+		#endregion
+
+		#region Formatting
+
+		///
+		/// The print forum name.
+		///
+		/// <param name="row">
+		/// The row.
+		/// </param>
+		/// <returns>
+		/// The print forum name.
+		/// </returns>
+		protected string PrintForumName(DataRowView row)
+		{
+			var forumName = (string)row["ForumName"];
+			string html = string.Empty;
+			if (forumName != this._lastForumName)
+			{
+				html = String.Format(
+				  @"
{0}
",
+				  forumName,
+				  YafBuildLink.GetLink(ForumPages.topics, "f={0}", row["ForumID"]));
+				this._lastForumName = forumName;
+			}
+
+			return html;
+		}
+
+		#endregion
+
+	}
 }


Parse terrain data from 0x30

Parse terrain data from 0x30

Parse terrain groups, triangles, and types

--- BlastCorpsEditor/BlastCorpsLevel.cs
+++ BlastCorpsEditor/BlastCorpsLevel.cs
@@ -102,6 +102,39 @@
       }
    }

+   public class TerrainTri
+   {
+      public Int16 x1, y1, z1;
+      public Int16 x2, y2, z2;
+      public Int16 x3, y3, z3;
+      public byte b12, b13;
+
+      public TerrainTri(Int16 x1, Int16 y1, Int16 z1, Int16 x2, Int16 y2, Int16 z2, Int16 x3, Int16 y3, Int16 z3, byte b12, byte b13)
+      {
+         this.x1 = x1;
+         this.y1 = y1;
+         this.z1 = z1;
+         this.x2 = x2;
+         this.y2 = y2;
+         this.z2 = z2;
+         this.x3 = x3;
+         this.y3 = y3;
+         this.z3 = z3;
+         this.b12 = b12;
+         this.b13 = b13;
+      }
+
+      public override string ToString()
+      {
+         return "(" + x1 + "," + y1 + "," + z1 + ") (" + x2 + "," + y2 + "," + z2 + ") (" + x3 + "," + y3 + "," + z3 + ") " + b12 + ":" + b13;
+      }
+   }
+
+   public class TerrainGroup
+   {
+      public List<TerrainTri> triangles = new List<TerrainTri>();
+   }
+
    public class LevelBounds
    {
       public Int16 x1, z1, x2, z2;
@@ -295,6 +328,7 @@
       public List<Collision24> collisions = new List<Collision24>();
       public List<CommPoint> commPoints = new List<CommPoint>();
       public List<Object2C> object2Cs = new List<Object2C>();
+      public List<TerrainGroup> terrainGroups = new List<TerrainGroup>();
       public List<RDU> rdus = new List<RDU>();
       public List<TNTCrate> tntCrates = new List<TNTCrate>();
       public List<SquareBlock> squareBlocks = new List<SquareBlock>();
@@ -304,7 +338,6 @@
       public List<Vehicle> vehicles = new List<Vehicle>();
       public Carrier carrier = new Carrier();
       public List<Building> buildings = new List<Building>();
-      private byte[] copy30;
       private byte[] copy48;
       private byte[] copy58;
       private byte[] copy60;
@@ -431,10 +464,36 @@
          }
       }

-      // 0x30: TODO
-      private void decode30(byte[] data)
-      {
-         copy30 = ArraySlice(data, BE.U32(data, 0x30), BE.U32(data, 0x34));
+      // 0x30: Terrain data
+      // [WW WW WW WW] {[X1 X1] [Y1 Y1] [Z1 Z1] [X2 X2] [Y2 Y2] [Z2 Z2] [X3 X3] [Y3 Y3] [Z3 Z3] [AA] [BB]}
+      private void decodeTerrain(byte[] data)
+      {
+         uint start = BE.U32(data, 0x30);
+         uint end = BE.U32(data, 0x34);
+         uint idx = start;
+         uint next;
+         while (idx < end)
+         {
+            next = start + BE.U32(data, idx);
+            idx += 4;
+            TerrainGroup tg = new TerrainGroup();
+            while (idx < next)
+            {
+               Int16 x1, y1, z1, x2, y2, z2, x3, y3, z3;
+               x1 = BE.I16(data, idx);
+               y1 = BE.I16(data, idx + 2);
+               z1 = BE.I16(data, idx + 4);
+               x2 = BE.I16(data, idx + 6);
+               y2 = BE.I16(data, idx + 8);
+               z2 = BE.I16(data, idx + 0xA);
+               x3 = BE.I16(data, idx + 0xC);
+               y3 = BE.I16(data, idx + 0xE);
+               z3 = BE.I16(data, idx + 0x10);
+               tg.triangles.Add(new TerrainTri(x1, y1, z1, x2, y2, z2, x3, y3, z3, data[idx + 0x12], data[idx + 0x13]));
+               idx += 0x14;
+            }
+            terrainGroups.Add(tg);
+         }
       }

       // 0x34: RDUs
@@ -838,9 +897,26 @@
             }
          }

-         // TODO: 0x30 real data
          BE.ToBytes(offset, data, 0x30);
-         offset += AppendArray(data, offset, copy30);
+         int first = offset;
+         foreach (TerrainGroup tg in terrainGroups)
+         {
+            offset += BE.ToBytes(offset - first + tg.triangles.Count * 0x14 + 4, data, offset);
+            foreach (TerrainTri tri in tg.triangles)
+            {
+               offset += BE.ToBytes(tri.x1, data, offset);
+               offset += BE.ToBytes(tri.y1, data, offset);
+               offset += BE.ToBytes(tri.z1, data, offset);
+               offset += BE.ToBytes(tri.x2, data, offset);
+               offset += BE.ToBytes(tri.y2, data, offset);
+               offset += BE.ToBytes(tri.z2, data, offset);
+               offset += BE.ToBytes(tri.x3, data, offset);
+               offset += BE.ToBytes(tri.y3, data, offset);
+               offset += BE.ToBytes(tri.z3, data, offset);
+               data[offset++] = tri.b12;
+               data[offset++] = tri.b13;
+            }
+         }

          BE.ToBytes(offset, data, 0x34);
          foreach (RDU rdu in rdus)
@@ -1043,7 +1119,7 @@
          level.decodeCollision24(levelData);     // 0x24
          level.decodeCommPoints(levelData);      // 0x28
          level.decode2C(levelData);              // 0x2C TODO: name
-         level.decode30(levelData);              // 0x30 TODO
+         level.decodeTerrain(levelData);         // 0x30
          level.decodeRDUs(levelData);            // 0x34
          level.decodeTNTCrates(levelData);       // 0x38
          level.decodeSquareBlocks(levelData);    // 0x3C