/// <summary> /// Initializes the patch bootstrapper, creating a PLibPatchRegistry if not yet /// present and offering our library as a candidate for shared patches. /// </summary> public static void Init() { var obj = Global.Instance.gameObject; if (obj != null) { // The hack is sick but we have few choices object reg = obj.GetComponent(typeof(PRegistry).Name); if (reg == null) { var plr = obj.AddComponent <PRegistry>(); // If PLib is ILMerged more than once, PRegistry gets added with a weird // type name including a GUID which does not match GetComponent.Name! string typeName = plr.GetType().Name; if (typeName != "PRegistry") { Debug.LogErrorFormat("PRegistry has the type name {0}; this may be " + "the result of ILMerging PLib more than once!", typeName); } #if DEBUG LogPatchDebug("Creating PLibRegistry from " + Assembly. GetExecutingAssembly().FullName); #endif // Patch in the bootstrap method plr.ApplyBootstrapper(); reg = plr; } // Use reflection to execute the actual AddPatch method try { Traverse.Create(reg).CallMethod(nameof(PRegistry.AddPatch), (object)new PLibPatches()); } catch (Exception e) { PUtil.LogException(e); } } else { #if DEBUG LogPatchWarning("Attempted to Init before Global created!"); #endif } }
/// <summary> /// Creates a ModInfoAttribute using an object from another mod. /// </summary> /// <param name="attr">The attribute from the other mod.</param> /// <returns>A ModInfoAttribute object with the values from that object, where /// possible to retrieve; or null if none could be obtained.</returns> internal static ModInfoAttribute CreateFrom(object attr) { string title = null, image = null, url = null; bool collapse = false; if (attr.GetType().Name == typeof(ModInfoAttribute).Name) { var trAttr = Traverse.Create(attr); try { title = trAttr.GetProperty <string>(nameof(Title)); image = trAttr.GetProperty <string>(nameof(Image)); url = trAttr.GetProperty <string>(nameof(URL)); collapse = trAttr.GetProperty <bool>(nameof(ForceCollapseCategories)); } catch (Exception e) { PUtil.LogExcWarn(e); } } return(string.IsNullOrEmpty(title) ? null : new ModInfoAttribute(title, url, image, collapse)); }
/// <summary> /// Creates an OptionAttribute using an object from another mod. /// </summary> /// <param name="attr">The attribute from the other mod.</param> /// <returns>An OptionAttribute object with the values from that object, where /// possible to retrieve; or null if none could be obtained.</returns> internal static OptionAttribute CreateFrom(object attr) { string title = "", tt = "", cat = "", format = null; if (attr.GetType().Name == typeof(OptionAttribute).Name) { var trAttr = Traverse.Create(attr); try { title = trAttr.GetProperty <string>(nameof(Title)); tt = trAttr.GetProperty <string>(nameof(Tooltip)) ?? ""; cat = trAttr.GetProperty <string>(nameof(Category)) ?? ""; format = trAttr.GetProperty <string>(nameof(Format)); } catch (Exception e) { PUtil.LogExcWarn(e); } } return(string.IsNullOrEmpty(title) ? null : new OptionAttribute(title, tt, cat) { Format = format }); }
/// <summary> /// Retrieves a type using its full name (including namespace). However, the assembly /// name is optional, as this method searches all assemblies in the current /// AppDomain if it is null or empty. /// </summary> /// <param name="name">The type name to retrieve.</param> /// <param name="assemblyName">If specified, the name of the assembly that contains /// the type. No other assembly name will be searched if this parameter is not null /// or empty. The assembly name might not match the DLL name, use a decompiler to /// make sure.</param> /// <returns>The type, or null if the type is not found or cannot be loaded.</returns> public static Type GetTypeSafe(string name, string assemblyName = null) { Type type = null; if (string.IsNullOrEmpty(assemblyName)) { foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) { try { type = assembly.GetType(name, false); } catch (System.IO.IOException) { // The common parent of exceptions when the type requires another type // that cannot be loaded } catch (BadImageFormatException) { } if (type != null) { break; } } } else { try { type = Type.GetType(name + ", " + assemblyName, false); } catch (TargetInvocationException e) { PUtil.LogWarning("Unable to load type {0} from assembly {1}:".F(name, assemblyName)); PUtil.LogExcWarn(e); } catch (ArgumentException e) { // A generic type is loaded with bad arguments PUtil.LogWarning("Unable to load type {0} from assembly {1}:".F(name, assemblyName)); PUtil.LogExcWarn(e); } catch (System.IO.IOException) { // The common parent of exceptions when the type requires another type that // cannot be loaded } catch (BadImageFormatException) { } } return(type); }
/// <summary> /// Creates a LimitAttribute using an object from another mod. /// </summary> /// <param name="attr">The attribute from the other mod.</param> /// <returns>A LimitAttribute object with the values from that object, where /// possible to retrieve; or null if none could be obtained.</returns> private static LimitAttribute CreateFrom(object attr) { LimitAttribute la = null; if (attr.GetType().Name == typeof(LimitAttribute).Name) { // Has limit type var trAttr = Traverse.Create(attr); double min = 0.0, max = 0.0; try { min = trAttr.GetProperty <double>(nameof(Minimum)); max = trAttr.GetProperty <double>(nameof(Maximum)); } catch (Exception e) { PUtil.LogExcWarn(e); } if (min != 0.0 || max != 0.0) { la = new LimitAttribute(min, max); } } return(la); }
/// <summary> /// Executes all legacy post-load handlers. /// </summary> internal static void ExecuteLegacyPostload() { IList <PostLoadHandler> postload = null; lock (PSharedData.GetLock(PRegistry.KEY_POSTLOAD_LOCK)) { // Get list holding postload information var list = PSharedData.GetData <IList <PostLoadHandler> >(PRegistry. KEY_POSTLOAD_TABLE); if (list != null) { postload = new List <PostLoadHandler>(list); } } // If there were any, run them if (postload != null) { var hInst = HarmonyInstance.Create("PLib.PostLoad"); PRegistry.LogPatchDebug("Executing {0:D} legacy post-load handler(s)".F( postload.Count)); foreach (var handler in postload) { try { handler?.Invoke(hInst); } catch (Exception e) { var method = handler.Method; // Say which mod's postload crashed if (method != null) { PRegistry.LogPatchWarning("Postload handler for mod {0} failed:".F( method.DeclaringType.Assembly?.GetName()?.Name ?? "?")); } PUtil.LogException(e); } } } }
/// <summary> /// Patches a constructor manually. /// </summary> /// <param name="instance">The Harmony instance.</param> /// <param name="type">The class to modify.</param> /// <param name="arguments">The constructor's argument types.</param> /// <param name="prefix">The prefix to apply, or null if none.</param> /// <param name="postfix">The postfix to apply, or null if none.</param> public static void PatchConstructor(this HarmonyInstance instance, Type type, Type[] arguments, HarmonyMethod prefix = null, HarmonyMethod postfix = null) { if (type == null) { throw new ArgumentNullException("type"); } // Fetch the constructor try { var cons = type.GetConstructor(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance, null, arguments, null); if (cons != null) { instance.Patch(cons, prefix, postfix); } else { PUtil.LogWarning("Unable to find constructor on type {0}".F(type. FullName)); } } catch (ArgumentException e) { PUtil.LogException(e); } }
/// <summary> /// Applies all patches. /// </summary> /// <param name="instance">The Harmony instance to use when patching.</param> private static void PatchAll(HarmonyInstance instance) { if (instance == null) { throw new ArgumentNullException("instance"); } // ColonyAchievementStatus instance.Patch(typeof(ColonyAchievementStatus), "Serialize", PatchMethod(nameof(Serialize_Prefix)), null); // Db instance.Patch(typeof(Db), "Initialize", PatchMethod(nameof(Initialize_Prefix)), PatchMethod(nameof(Initialize_Postfix))); // Game instance.Patch(typeof(Game), "DestroyInstances", null, PatchMethod(nameof( Game_DestroyInstances_Postfix))); instance.Patch(typeof(Game), "OnPrefabInit", null, PatchMethod(nameof( Game_OnPrefabInit_Postfix))); // GameInputMapping instance.Patch(typeof(GameInputMapping), "SetDefaultKeyBindings", null, PatchMethod(nameof(SetDefaultKeyBindings_Postfix))); // GameUtil instance.Patch(typeof(GameUtil), "GetKeycodeLocalized", PatchMethod(nameof(GetKeycodeLocalized_Prefix)), null); // KInputController instance.PatchConstructor(typeof(KInputController.KeyDef), new Type[] { typeof(KKeyCode), typeof(Modifier) }, null, PatchMethod(nameof(CKeyDef_Postfix))); instance.Patch(typeof(KInputController), "IsActive", PatchMethod(nameof(IsActive_Prefix)), null); instance.Patch(typeof(KInputController), "QueueButtonEvent", PatchMethod(nameof(QueueButtonEvent_Prefix)), null); if (PLightManager.InitInstance()) { // DiscreteShadowCaster instance.Patch(typeof(DiscreteShadowCaster), "GetVisibleCells", PatchMethod(nameof(GetVisibleCells_Prefix)), null); // Light2D instance.Patch(typeof(Light2D), "AddToScenePartitioner", PatchMethod(nameof(AddToScenePartitioner_Prefix)), null); instance.Patch(typeof(Light2D), "RefreshShapeAndPosition", null, PatchMethod(nameof(RefreshShapeAndPosition_Postfix))); // LightGridEmitter instance.Patch(typeof(LightGridEmitter), "AddToGrid", null, PatchMethod(nameof(AddToGrid_Postfix))); instance.Patch(typeof(LightGridEmitter), "ComputeLux", PatchMethod(nameof(ComputeLux_Prefix)), null); instance.Patch(typeof(LightGridEmitter), "RemoveFromGrid", null, PatchMethod(nameof(RemoveFromGrid_Postfix))); instance.Patch(typeof(LightGridEmitter), "UpdateLitCells", PatchMethod(nameof(UpdateLitCells_Prefix)), null); // LightGridManager instance.Patch(typeof(LightGridManager), "CreatePreview", PatchMethod(nameof(CreatePreview_Prefix)), null); // LightShapePreview instance.Patch(typeof(LightShapePreview), "Update", PatchMethod(nameof(LightShapePreview_Update_Prefix)), null); // Rotatable instance.Patch(typeof(Rotatable), "OrientVisualizer", null, PatchMethod(nameof(OrientVisualizer_Postfix))); } // MainMenu instance.Patch(typeof(MainMenu), "OnSpawn", null, PatchMethod( nameof(MainMenu_OnSpawn_Postfix))); // PBuilding instance.Patch(typeof(BuildingTemplates), "CreateBuildingDef", null, PatchMethod(nameof(CreateBuildingDef_Postfix))); instance.Patch(typeof(EquipmentTemplates), "CreateEquipmentDef", null, PatchMethod(nameof(CreateEquipmentDef_Postfix))); if (PBuilding.CheckBuildings()) { instance.Patch(typeof(GeneratedBuildings), "LoadGeneratedBuildings", PatchMethod(nameof(LoadGeneratedBuildings_Prefix)), null); } // PCodex instance.Patch(typeof(CodexCache), "CollectEntries", null, PatchMethod(nameof(CollectEntries_Postfix))); instance.Patch(typeof(CodexCache), "CollectSubEntries", null, PatchMethod(nameof(CollectSubEntries_Postfix))); // PLocalization var locale = Localization.GetLocale(); if (locale != null) { PLocalization.LocalizeAll(locale); } // ModsScreen POptions.Init(); instance.Patch(typeof(ModsScreen), "BuildDisplay", null, PatchMethod(nameof(BuildDisplay_Postfix))); // SteamUGCService var ugc = PPatchTools.GetTypeSafe("SteamUGCService", "Assembly-CSharp"); if (ugc != null) { try { instance.PatchTranspile(ugc, "LoadPreviewImage", PatchMethod(nameof( LoadPreviewImage_Transpile))); } catch (Exception e) { PUtil.LogExcWarn(e); } } // TMPro.TMP_InputField try { instance.Patch(typeof(TMPro.TMP_InputField), "OnEnable", null, PatchMethod(nameof(OnEnable_Postfix))); } catch (Exception) { PUtil.LogWarning("Unable to patch TextMeshPro bug, text fields may display " + "improperly inside scroll areas"); } // Postload, legacy and normal PPatchManager.ExecuteLegacyPostload(); PPatchManager.RunAll(RunAt.AfterModsLoad); }
/// <summary> /// Applies all patches. /// </summary> /// <param name="instance">The Harmony instance to use when patching.</param> private static void PatchAll(HarmonyInstance instance) { if (instance == null) { throw new ArgumentNullException("instance"); } // ColonyAchievementStatus instance.Patch(typeof(ColonyAchievementStatus), "Serialize", PatchMethod(nameof(Serialize_Prefix)), null); // GameInputMapping instance.Patch(typeof(GameInputMapping), "SetDefaultKeyBindings", null, PatchMethod(nameof(SetDefaultKeyBindings_Postfix))); // KInputController instance.PatchConstructor(typeof(KInputController.KeyDef), new Type[] { typeof(KKeyCode), typeof(Modifier) }, null, PatchMethod(nameof(CKeyDef_Postfix))); instance.Patch(typeof(KInputController), "IsActive", PatchMethod(nameof(IsActive_Prefix)), null); instance.Patch(typeof(KInputController), "QueueButtonEvent", PatchMethod(nameof(QueueButtonEvent_Prefix)), null); if (PLightManager.InitInstance()) { // DiscreteShadowCaster instance.Patch(typeof(DiscreteShadowCaster), "GetVisibleCells", PatchMethod(nameof(GetVisibleCells_Prefix)), null); // Light2D instance.Patch(typeof(Light2D), "AddToScenePartitioner", PatchMethod(nameof(AddToScenePartitioner_Prefix)), null); instance.Patch(typeof(Light2D), "RefreshShapeAndPosition", null, PatchMethod(nameof(RefreshShapeAndPosition_Postfix))); // LightGridEmitter instance.Patch(typeof(LightGridEmitter), "AddToGrid", null, PatchMethod(nameof(AddToGrid_Postfix))); instance.Patch(typeof(LightGridEmitter), "ComputeLux", PatchMethod(nameof(ComputeLux_Prefix)), null); instance.Patch(typeof(LightGridEmitter), "RemoveFromGrid", null, PatchMethod(nameof(RemoveFromGrid_Postfix))); instance.Patch(typeof(LightGridEmitter), "UpdateLitCells", PatchMethod(nameof(UpdateLitCells_Prefix)), null); // LightGridManager instance.Patch(typeof(LightGridManager), "CreatePreview", PatchMethod(nameof(CreatePreview_Prefix)), null); // LightShapePreview instance.Patch(typeof(LightShapePreview), "Update", PatchMethod(nameof(LightShapePreview_Update_Prefix)), null); // Rotatable instance.Patch(typeof(Rotatable), "OrientVisualizer", null, PatchMethod(nameof(OrientVisualizer_Postfix))); } // PBuilding instance.Patch(typeof(BuildingTemplates), "CreateBuildingDef", null, PatchMethod(nameof(CreateBuildingDef_Postfix))); instance.Patch(typeof(EquipmentTemplates), "CreateEquipmentDef", null, PatchMethod(nameof(CreateEquipmentDef_Postfix))); if (PBuilding.CheckBuildings()) { instance.Patch(typeof(Db), "Initialize", PatchMethod(nameof(Initialize_Prefix)), null); instance.Patch(typeof(GeneratedBuildings), "LoadGeneratedBuildings", PatchMethod(nameof(LoadGeneratedBuildings_Prefix)), null); } // PCodex instance.Patch(typeof(CodexCache), "CollectEntries", null, PatchMethod(nameof(CollectEntries_Postfix))); instance.Patch(typeof(CodexCache), "CollectSubEntries", null, PatchMethod(nameof(CollectSubEntries_Postfix))); // PLocalization var locale = Localization.GetLocale(); if (locale != null) { PLocalization.LocalizeAll(locale); } // ModsScreen POptions.Init(); instance.Patch(typeof(ModsScreen), "BuildDisplay", null, PatchMethod(nameof(BuildDisplay_Postfix))); // SteamUGCService try { instance.PatchTranspile(typeof(SteamUGCService), "LoadPreviewImage", PatchMethod(nameof(LoadPreviewImage_Transpile))); } catch (TypeLoadException) { // Not a Steam install, ignoring } // Postload PUtil.ExecutePostload(); }
/// <summary> /// Transpiles a method to replace calls to the specified victim methods with /// replacement methods, altering the call type if necessary. /// /// Each key to value pair must meet the criteria defined in /// ReplaceMethodCall(TranspiledMethod, MethodInfo, MethodInfo). /// </summary> /// <param name="method">The method to patch.</param> /// <param name="translation">A mapping from the old method calls to replace, to the /// new method calls to use instead.</param> /// <returns>A transpiled version of that method that replaces or removes all calls /// to the specified methods.</returns> /// <exception cref="ArgumentException">If any of the new methods' argument types do /// not exactly match the old methods' argument types.</exception> public static TranspiledMethod ReplaceMethodCall(TranspiledMethod method, IDictionary <MethodInfo, MethodInfo> translation) { if (method == null) { throw new ArgumentNullException("method"); } if (translation == null) { throw new ArgumentNullException("translation"); } // Sanity check arguments int replaced = 0; foreach (var pair in translation) { var victim = pair.Key; var newMethod = pair.Value; if (victim == null) { throw new ArgumentNullException("victim"); } if (newMethod != null) { PTranspilerTools.CompareMethodParams(victim, victim.GetParameterTypes(), newMethod); } else if (victim.ReturnType != typeof(void)) { throw new ArgumentException("Cannot remove method {0} with a return value". F(victim.Name)); } } foreach (var instruction in method) { var opcode = instruction.opcode; MethodInfo target; if ((opcode == OpCodes.Call || opcode == OpCodes.Calli || opcode == OpCodes. Callvirt) && translation.TryGetValue(target = instruction.operand as MethodInfo, out MethodInfo newMethod)) { if (newMethod != null) { // Replace with new method instruction.opcode = newMethod.IsStatic ? OpCodes.Call : OpCodes.Callvirt; instruction.operand = newMethod; yield return(instruction); } else { // Pop "this" if needed int n = target.GetParameters().Length; if (!target.IsStatic) { n++; } // Pop the arguments off the stack instruction.opcode = (n == 0) ? OpCodes.Nop : OpCodes.Pop; instruction.operand = null; yield return(instruction); for (int i = 0; i < n - 1; i++) { yield return(new CodeInstruction(OpCodes.Pop)); } } replaced++; } else { yield return(instruction); } } #if DEBUG if (replaced == 0) { if (translation.Count == 1) { // Diagnose the method that could not be replaced var items = new KeyValuePair <MethodInfo, MethodInfo> [1]; translation.CopyTo(items, 0); MethodInfo from = items[0].Key, to = items[0].Value; PUtil.LogWarning("No method calls replaced: {0}.{1} to {2}.{3}".F( from.DeclaringType.FullName, from.Name, to.DeclaringType.FullName, to.Name)); } else { PUtil.LogWarning("No method calls replaced (multiple replacements)"); } } #endif }
public static void LogAllFailedAsserts() { PUtil.LogWarning("PLib in mod " + Assembly.GetCallingAssembly().GetName()?.Name + " is logging ALL failed assertions!"); PTranspilerTools.LogAllFailedAsserts(); }
public static void LogAllExceptions() { PUtil.LogWarning("PLib in mod " + Assembly.GetCallingAssembly().GetName()?.Name + " is logging ALL unhandled exceptions!"); PTranspilerTools.LogAllExceptions(); }
/// <summary> /// Applies all patches. /// </summary> /// <param name="instance">The Harmony instance to use when patching.</param> private static void PatchAll(HarmonyInstance instance) { if (instance == null) { throw new ArgumentNullException("instance"); } // GameInputMapping instance.Patch(typeof(GameInputMapping), "SetDefaultKeyBindings", null, PatchMethod("SetDefaultKeyBindings_Postfix")); // KInputController instance.PatchConstructor(typeof(KInputController.KeyDef), new Type[] { typeof(KKeyCode), typeof(Modifier) }, null, PatchMethod("CKeyDef_Postfix")); instance.Patch(typeof(KInputController), "IsActive", PatchMethod("IsActive_Prefix"), null); instance.Patch(typeof(KInputController), "QueueButtonEvent", PatchMethod("QueueButtonEvent_Prefix"), null); if (PLightManager.InitInstance()) { // DiscreteShadowCaster instance.Patch(typeof(DiscreteShadowCaster), "GetVisibleCells", PatchMethod("GetVisibleCells_Prefix"), null); // Light2D instance.Patch(typeof(Light2D), "AddToScenePartitioner", PatchMethod("AddToScenePartitioner_Prefix"), null); instance.Patch(typeof(Light2D), "RefreshShapeAndPosition", null, PatchMethod("RefreshShapeAndPosition_Postfix")); // LightGridEmitter instance.Patch(typeof(LightGridEmitter), "AddToGrid", null, PatchMethod("AddToGrid_Postfix")); instance.Patch(typeof(LightGridEmitter), "ComputeLux", PatchMethod("ComputeLux_Prefix"), null); instance.Patch(typeof(LightGridEmitter), "RemoveFromGrid", null, PatchMethod("RemoveFromGrid_Postfix")); instance.Patch(typeof(LightGridEmitter), "UpdateLitCells", PatchMethod("UpdateLitCells_Prefix"), null); // LightGridManager instance.Patch(typeof(LightGridManager), "CreatePreview", PatchMethod("CreatePreview_Prefix"), null); // LightShapePreview instance.Patch(typeof(LightShapePreview), "Update", PatchMethod("LightShapePreview_Update_Prefix"), null); // Rotatable instance.Patch(typeof(Rotatable), "OrientVisualizer", null, PatchMethod("OrientVisualizer_Postfix")); } // ModsScreen POptions.Init(); instance.Patch(typeof(ModsScreen), "BuildDisplay", null, PatchMethod("BuildDisplay_Postfix")); // SteamUGCService try { instance.PatchTranspile(typeof(SteamUGCService), "LoadPreviewImage", PatchMethod("LoadPreviewImage_Transpile")); } catch (TypeLoadException) { // Not a Steam install, ignoring } // Postload PUtil.ExecutePostload(); }
/// <summary> /// Transpiles a method to replace all calls to the specified victim method with /// another method, altering the call type if necessary. The argument types and return /// type must match exactly, including in/out/ref parameters. /// /// If replacing an instance method call with a static method, the first argument /// will receive the "this" which the old method would have received. /// /// If newMethod is null, the calls will all be removed silently instead. This will /// fail if the method call being removed had a return type (what would it be replaced /// with?); in those cases, declare an empty method with the same signature and /// replace it instead. /// </summary> /// <param name="method">The method to patch.</param> /// <param name="victim">The old method calls to remove.</param> /// <param name="newMethod">The new method to replace, or null to delete the calls.</param> /// <returns>A transpiled version of that method that replaces or removes all calls /// to method.</returns> /// <exception cref="ArgumentException">If the new method's argument types do not /// exactly match the old method's argument types.</exception> public static TranspiledMethod ReplaceMethodCall(TranspiledMethod method, MethodInfo victim, MethodInfo newMethod = null) { if (method == null) { throw new ArgumentNullException("method"); } if (victim == null) { throw new ArgumentNullException("victim"); } // Sanity check arguments var types = victim.GetParameterTypes(); int n = types.Length, replaced = 0; if (newMethod != null) { CompareMethodParams(victim, types, newMethod); } else if (victim.ReturnType != typeof(void)) { throw new ArgumentException("Cannot remove method {0} with a return value".F( victim.Name)); } // Pop "this" in removal cases if (!victim.IsStatic) { n++; } foreach (var instruction in method) { var opcode = instruction.opcode; if ((opcode == OpCodes.Call || opcode == OpCodes.Calli || opcode == OpCodes. Callvirt) && instruction.operand == victim) { if (newMethod != null) { // Replace with new method instruction.opcode = newMethod.IsStatic ? OpCodes.Call : OpCodes.Callvirt; instruction.operand = newMethod; yield return(instruction); } else { // Pop the arguments off the stack instruction.opcode = (n == 0) ? OpCodes.Nop : OpCodes.Pop; instruction.operand = null; yield return(instruction); for (int i = 0; i < n - 1; i++) { yield return(new CodeInstruction(OpCodes.Pop)); } } replaced++; } else { yield return(instruction); } } #if DEBUG if (replaced == 0) { PUtil.LogWarning("No method calls replaced: {0}.{1} to {2}.{3}".F(victim. DeclaringType.Name, victim.Name, newMethod?.DeclaringType?.Name ?? "None", newMethod?.Name)); } #endif }
static PAction() { // Enum.TryParse was introduced in .NET Framework 4.0 MaxAction = PUtil.TryParseEnum("NumActions", Action.NumActions); }