protected virtual void CreateModMenuSectionHeader(TextMenu menu, bool inGame, EventInstance snapshot) { Type type = SettingsType; EverestModuleSettings settings = _Settings; if (type == null || settings == null) { return; } string typeName = type.Name.ToLowerInvariant(); if (typeName.EndsWith("settings")) { typeName = typeName.Substring(0, typeName.Length - 8); } string nameDefaultPrefix = $"modoptions_{typeName}_"; string name; // We lazily reuse this field for the props later on. name = type.GetCustomAttribute <SettingNameAttribute>()?.Name ?? $"{nameDefaultPrefix}title"; name = name.DialogCleanOrNull() ?? Metadata.Name.SpacedPascalCase(); menu.Add(new patch_TextMenu.patch_SubHeader(name + " | v." + Metadata.VersionString)); }
/// <summary> /// Load the mod settings. Loads the settings from {UserIO.GetSavePath("Saves")}/modsettings-{Metadata.Name}.celeste by default. /// </summary> public virtual void LoadSettings() { if (SettingsType == null) { return; } _Settings = (EverestModuleSettings)SettingsType.GetConstructor(Everest._EmptyTypeArray).Invoke(Everest._EmptyObjectArray); string path = patch_UserIO.GetSaveFilePath("modsettings-" + Metadata.Name); // Temporary fallback to help migrate settings from their old location. if (!File.Exists(path)) { path = Path.Combine(Everest.PathEverest, "ModSettings-OBSOLETE", Metadata.Name + ".yaml"); } if (!File.Exists(path)) { return; } try { using (Stream stream = File.OpenRead(path)) { if (_Settings is EverestModuleBinarySettings) { using (BinaryReader reader = new BinaryReader(stream)) ((EverestModuleBinarySettings)_Settings).Read(reader); } else { IDeserializer deserializer = new DeserializerBuilder() .IgnoreUnmatchedProperties() .WithObjectFactory(t => _Settings) .Build(); using (StreamReader reader = new StreamReader(path)) _Settings = (EverestModuleSettings)deserializer.Deserialize(reader, SettingsType); } } } catch { } if (_Settings == null) { _Settings = (EverestModuleSettings)SettingsType.GetConstructor(Everest._EmptyTypeArray).Invoke(Everest._EmptyObjectArray); } }
/// <summary> /// Load the mod settings. Loads the settings from {Everest.PathSettings}/{Metadata.Name}.yaml by default. /// </summary> public virtual void LoadSettings() { if (SettingsType == null) { return; } _Settings = (EverestModuleSettings)SettingsType.GetConstructor(Everest._EmptyTypeArray).Invoke(Everest._EmptyObjectArray); string extension = ".yaml"; if (_Settings is EverestModuleBinarySettings) { extension = ".bin"; } string path = Path.Combine(Everest.PathSettings, Metadata.Name + extension); if (!File.Exists(path)) { return; } try { using (Stream stream = File.OpenRead(path)) { if (_Settings is EverestModuleBinarySettings) { // .bin using (BinaryReader reader = new BinaryReader(stream)) ((EverestModuleBinarySettings)_Settings).Read(reader); } else { // .yaml using (StreamReader reader = new StreamReader(path)) { _Settings = (EverestModuleSettings)YamlHelper.Deserializer.Deserialize(reader, SettingsType); } } } } catch { } if (_Settings == null) { _Settings = (EverestModuleSettings)SettingsType.GetConstructor(Everest._EmptyTypeArray).Invoke(Everest._EmptyObjectArray); } }
/// <summary> /// Load the mod settings. Loads the settings from {Everest.PathSettings}/{Metadata.Name}.yaml by default. /// </summary> public virtual void LoadSettings() { if (SettingsType == null) { return; } string path = Path.Combine(Everest.PathSettings, Metadata.Name + ".yaml"); if (!File.Exists(path)) { _Settings = (EverestModuleSettings)SettingsType.GetConstructor(Everest._EmptyTypeArray).Invoke(Everest._EmptyObjectArray); return; } using (Stream stream = File.OpenRead(path)) using (StreamReader reader = new StreamReader(path)) _Settings = (EverestModuleSettings)YamlHelper.Deserializer.Deserialize(reader, SettingsType); }
/// <summary> /// Load the mod settings. Loads the settings from {UserIO.GetSavePath("Saves")}/modsettings-{Metadata.Name}.celeste by default. /// </summary> public virtual void LoadSettings() { if (SettingsType == null) { return; } _Settings = (EverestModuleSettings)SettingsType.GetConstructor(Everest._EmptyTypeArray).Invoke(Everest._EmptyObjectArray); string path = patch_UserIO.GetSaveFilePath("modsettings-" + Metadata.Name); // Temporary fallback to help migrate settings from their old location. if (!File.Exists(path)) { path = Path.Combine(Everest.PathEverest, "ModSettings-OBSOLETE", Metadata.Name + ".yaml"); } if (!File.Exists(path)) { return; } try { using (Stream stream = File.OpenRead(path)) { if (_Settings is EverestModuleBinarySettings) { using (BinaryReader reader = new BinaryReader(stream)) ((EverestModuleBinarySettings)_Settings).Read(reader); } else { using (StreamReader reader = new StreamReader(stream)) YamlHelper.DeserializerUsing(_Settings).Deserialize(reader, SettingsType); } } } catch (Exception e) { Logger.Log(LogLevel.Warn, "EverestModule", $"Failed to load the settings of {Metadata.Name}!"); Logger.LogDetailed(e); } if (_Settings == null) { _Settings = (EverestModuleSettings)SettingsType.GetConstructor(Everest._EmptyTypeArray).Invoke(Everest._EmptyObjectArray); } }
/// <summary> /// Create the mod menu subsection including the section header in the given menu. /// The default implementation uses reflection to attempt creating a menu. /// </summary> /// <param name="menu">Menu to add the section to.</param> /// <param name="inGame">Whether we're in-game (paused) or in the main menu.</param> /// <param name="snapshot">The Level.PauseSnapshot</param> public virtual void CreateModMenuSection(TextMenu menu, bool inGame, EventInstance snapshot) { Type type = SettingsType; EverestModuleSettings settings = _Settings; if (type == null || settings == null) { return; } // The default name prefix. string typeName = type.Name.ToLowerInvariant(); if (typeName.EndsWith("settings")) { typeName = typeName.Substring(0, typeName.Length - 8); } string nameDefaultPrefix = $"modoptions_{typeName}_"; // Any attributes we may want to get and read from later. SettingInGameAttribute attribInGame; SettingRangeAttribute attribRange; SettingNumberInputAttribute attribNumber; // If the settings type has got the InGame attrib, only show it in the matching situation. if ((attribInGame = type.GetCustomAttribute <SettingInGameAttribute>()) != null && attribInGame.InGame != inGame) { return; } bool headerCreated = false; if (GetType().GetMethod("CreateModMenuSection").DeclaringType != typeof(EverestModule)) { CreateModMenuSectionHeader(menu, inGame, snapshot); headerCreated = true; } PropertyInfo[] props; if (type == _PrevSettingsType) { props = _PrevSettingsProps; } else { _PrevSettingsProps = props = type.GetProperties(); _PrevSettingsType = type; } foreach (PropertyInfo prop in props) { MethodInfo creator = type.GetMethod( $"Create{prop.Name}Entry", BindingFlags.Public | BindingFlags.Instance, null, new Type[] { typeof(TextMenu), typeof(bool) }, new ParameterModifier[0] ); if (creator != null) { if (!headerCreated) { CreateModMenuSectionHeader(menu, inGame, snapshot); headerCreated = true; } creator.GetFastDelegate()(settings, menu, inGame); continue; } if ((attribInGame = prop.GetCustomAttribute <SettingInGameAttribute>()) != null && attribInGame.InGame != inGame) { continue; } if (prop.GetCustomAttribute <SettingIgnoreAttribute>() != null) { continue; } if (!prop.CanRead || !prop.CanWrite) { continue; } string name = prop.GetCustomAttribute <SettingNameAttribute>()?.Name ?? $"{nameDefaultPrefix}{prop.Name.ToLowerInvariant()}"; name = name.DialogCleanOrNull() ?? prop.Name.SpacedPascalCase(); bool needsRelaunch = prop.GetCustomAttribute <SettingNeedsRelaunchAttribute>() != null; string description = prop.GetCustomAttribute <SettingSubTextAttribute>()?.Description; TextMenu.Item item = null; Type propType = prop.PropertyType; object value = prop.GetValue(settings); // Create the matching item based off of the type and attributes. if (propType == typeof(bool)) { item = new TextMenu.OnOff(name, (bool)value) .Change(v => prop.SetValue(settings, v)) ; } else if ( propType == typeof(int) && (attribRange = prop.GetCustomAttribute <SettingRangeAttribute>()) != null ) { if (attribRange.LargeRange) { item = new TextMenuExt.IntSlider(name, attribRange.Min, attribRange.Max, (int)value) .Change(v => prop.SetValue(settings, v)) ; } else { item = new TextMenu.Slider(name, i => i.ToString(), attribRange.Min, attribRange.Max, (int)value) .Change(v => prop.SetValue(settings, v)) ; } } else if ((propType == typeof(int) || propType == typeof(float)) && (attribNumber = prop.GetCustomAttribute <SettingNumberInputAttribute>()) != null) { float currentValue; Action <float> valueSetter; if (propType == typeof(int)) { currentValue = (int)value; valueSetter = v => prop.SetValue(settings, (int)v); } else { currentValue = (float)value; valueSetter = v => prop.SetValue(settings, v); } int maxLength = attribNumber.MaxLength; bool allowNegatives = attribNumber.AllowNegatives; item = new TextMenu.Button(name + ": " + currentValue.ToString($"F{maxLength}").TrimEnd('0').TrimEnd('.')) .Pressed(() => { Audio.Play(SFX.ui_main_savefile_rename_start); menu.SceneAs <Overworld>().Goto <OuiNumberEntry>().Init <OuiModOptions>( currentValue, valueSetter, maxLength, propType == typeof(float), allowNegatives ); }) ; } else if (propType.IsEnum) { Array enumValues = Enum.GetValues(propType); Array.Sort((int[])enumValues); string enumNamePrefix = $"{nameDefaultPrefix}{prop.Name.ToLowerInvariant()}_"; item = new TextMenu.Slider(name, (i) => { string enumName = enumValues.GetValue(i).ToString(); return ($"{enumNamePrefix}{enumName.ToLowerInvariant()}".DialogCleanOrNull() ?? $"modoptions_{propType.Name.ToLowerInvariant()}_{enumName.ToLowerInvariant()}".DialogCleanOrNull() ?? enumName); }, 0, enumValues.Length - 1, (int)value) .Change(v => prop.SetValue(settings, v)) ; } else if (!inGame && propType == typeof(string)) { int maxValueLength = prop.GetCustomAttribute <SettingMaxLengthAttribute>()?.Max ?? 12; int minValueLength = prop.GetCustomAttribute <SettingMinLengthAttribute>()?.Min ?? 1; item = new TextMenu.Button(name + ": " + value) .Pressed(() => { Audio.Play(SFX.ui_main_savefile_rename_start); menu.SceneAs <Overworld>().Goto <OuiModOptionString>().Init <OuiModOptions>( (string)value, v => prop.SetValue(settings, v), maxValueLength, minValueLength ); }) ; } if (item == null) { continue; } if (!headerCreated) { CreateModMenuSectionHeader(menu, inGame, snapshot); headerCreated = true; } menu.Add(item); if (needsRelaunch) { item = item.NeedsRelaunch(menu); } if (description != null) { item = item.AddDescription(menu, description.DialogCleanOrNull() ?? description); } } foreach (PropertyInfo prop in type.GetProperties()) { if ((attribInGame = prop.GetCustomAttribute <SettingInGameAttribute>()) != null && attribInGame.InGame != inGame) { continue; } if (prop.GetCustomAttribute <SettingIgnoreAttribute>() != null) { continue; } if (!prop.CanRead || !prop.CanWrite) { continue; } if (!typeof(ButtonBinding).IsAssignableFrom(prop.PropertyType)) { continue; } if (!headerCreated) { CreateModMenuSectionHeader(menu, inGame, snapshot); headerCreated = true; } CreateModMenuSectionKeyBindings(menu, inGame, snapshot); break; } }
/// <summary> /// Create the mod menu subsection including the section header in the given menu. /// The default implementation uses reflection to attempt creating a menu. /// </summary> /// <param name="menu">Menu to add the section to.</param> /// <param name="inGame">Whether we're in-game (paused) or in the main menu.</param> /// <param name="snapshot">The Level.PauseSnapshot</param> public virtual void CreateModMenuSection(TextMenu menu, bool inGame, EventInstance snapshot) { Type type = SettingsType; EverestModuleSettings settings = _Settings; if (type == null || settings == null) { return; } // The default name prefix. string typeName = type.Name.ToLowerInvariant(); if (typeName.EndsWith("settings")) { typeName = typeName.Substring(0, typeName.Length - 8); } string nameDefaultPrefix = $"modoptions_{typeName}_"; // Any attributes we may want to get and read from later. SettingInGameAttribute attribInGame; SettingRangeAttribute attribRange; // If the settings type has got the InGame attrib, only show it in the matching situation. if ((attribInGame = type.GetCustomAttribute <SettingInGameAttribute>()) != null && attribInGame.InGame != inGame) { return; } // The settings subheader. string name; // We lazily reuse this field for the props later on. name = type.GetCustomAttribute <SettingNameAttribute>()?.Name ?? $"{nameDefaultPrefix}title"; name = name.DialogCleanOrNull() ?? Metadata.Name.SpacedPascalCase(); menu.Add(new TextMenu.SubHeader(name + " | v." + Metadata.VersionString)); PropertyInfo[] props; if (type == _PrevSettingsType) { props = _PrevSettingsProps; } else { _PrevSettingsProps = props = type.GetProperties(); _PrevSettingsType = type; } foreach (PropertyInfo prop in props) { MethodInfo creator = type.GetMethod( $"Create{prop.Name}Entry", BindingFlags.Public | BindingFlags.Instance, null, new Type[] { typeof(TextMenu), typeof(bool) }, new ParameterModifier[0] ); if (creator != null) { creator.GetFastDelegate()(settings, menu, inGame); continue; } if ((attribInGame = prop.GetCustomAttribute <SettingInGameAttribute>()) != null && attribInGame.InGame != inGame) { continue; } if (prop.GetCustomAttribute <SettingIgnoreAttribute>() != null) { continue; } if (!prop.CanRead || !prop.CanWrite) { continue; } name = prop.GetCustomAttribute <SettingNameAttribute>()?.Name ?? $"{nameDefaultPrefix}{prop.Name.ToLowerInvariant()}"; name = name.DialogCleanOrNull() ?? prop.Name.SpacedPascalCase(); bool needsRelaunch = prop.GetCustomAttribute <SettingNeedsRelaunchAttribute>() != null; TextMenu.Item item = null; Type propType = prop.PropertyType; object value = prop.GetValue(settings); // Create the matching item based off of the type and attributes. if (propType == typeof(bool)) { item = new TextMenu.OnOff(name, (bool)value) .Change(v => prop.SetValue(settings, v)) ; } else if ( propType == typeof(int) && (attribRange = prop.GetCustomAttribute <SettingRangeAttribute>()) != null ) { item = new TextMenu.Slider(name, i => i.ToString(), attribRange.Min, attribRange.Max, (int)value) .Change(v => prop.SetValue(settings, v)) ; } else if (propType.IsEnum) { Array enumValues = Enum.GetValues(propType); Array.Sort((int[])enumValues); string enumNamePrefix = $"{nameDefaultPrefix}{prop.Name.ToLowerInvariant()}_"; item = new TextMenu.Slider(name, (i) => { string enumName = enumValues.GetValue(i).ToString(); string fullName = $"{enumNamePrefix}{enumName.ToLowerInvariant()}"; return(fullName.DialogCleanOrNull() ?? enumName); }, 0, enumValues.Length - 1, (int)value) .Change(v => prop.SetValue(settings, v)) ; } else if (!inGame && propType == typeof(string)) { item = new TextMenu.Button(name + ": " + value) .Pressed(() => { Audio.Play(Sfxs.ui_main_savefile_rename_start); menu.SceneAs <Overworld>().Goto <OuiModOptionString>().Init <OuiModOptions>( (string)value, v => prop.SetValue(settings, v) ); }) ; } if (item == null) { continue; } if (needsRelaunch) { item = item.NeedsRelaunch(); } menu.Add(item); } }
/// <summary> /// Create the mod menu subsection including the section header in the given menu. /// The default implementation uses reflection to attempt creating a menu. /// </summary> /// <param name="menu">Menu to add the section to.</param> /// <param name="inGame">Whether we're in-game (paused) or in the main menu.</param> /// <param name="snapshot">The Level.PauseSnapshot</param> public virtual void CreateModMenuSection(TextMenu menu, bool inGame, EventInstance snapshot) { Type type = SettingsType; EverestModuleSettings settings = _Settings; if (type == null || settings == null) { return; } // The default name prefix. string nameDefaultPrefix = $"modoptions_{type.Name.ToLowerInvariant()}_"; if (nameDefaultPrefix.EndsWith("Settings")) { nameDefaultPrefix = nameDefaultPrefix.Substring(0, nameDefaultPrefix.Length - 8); } // Any attributes we may want to get and read from later. SettingInGameAttribute attribInGame; SettingRangeAttribute attribRange; // If the settings type has got the InGame attrib, only show it in the matching situation. if ((attribInGame = type.GetCustomAttribute <SettingInGameAttribute>()) != null && attribInGame.InGame != inGame) { return; } // The settings subheader. string name; // We lazily reuse this field for the props later on. name = type.GetCustomAttribute <SettingNameAttribute>()?.Name ?? $"{nameDefaultPrefix}title"; name = name.DialogCleanOrNull() ?? Metadata.Name.SpacedPascalCase(); menu.Add(new TextMenu.SubHeader(name)); PropertyInfo[] props; if (type == _PrevSettingsType) { props = _PrevSettingsProps; } else { _PrevSettingsProps = props = type.GetProperties(); _PrevSettingsType = type; } foreach (PropertyInfo prop in props) { if ((attribInGame = prop.GetCustomAttribute <SettingInGameAttribute>()) != null && attribInGame.InGame != inGame) { continue; } if (prop.GetCustomAttribute <SettingIgnoreAttribute>() != null) { continue; } if (!prop.CanRead || !prop.CanWrite) { continue; } name = prop.GetCustomAttribute <SettingNameAttribute>()?.Name ?? $"{nameDefaultPrefix}{prop.Name.ToLowerInvariant()}"; name = name.DialogCleanOrNull() ?? prop.Name.SpacedPascalCase(); bool needsRelaunch = prop.GetCustomAttribute <SettingNeedsRelaunchAttribute>() != null; TextMenu.Item item = null; Type propType = prop.PropertyType; object value = prop.GetValue(settings); // Create the matching item based off of the type and attributes. if (propType == typeof(bool)) { item = new TextMenu.OnOff(name, (bool)value) .Change(v => prop.SetValue(settings, v)) .NeedsRelaunch(needsRelaunch) ; } else if ( propType == typeof(int) && (attribRange = prop.GetCustomAttribute <SettingRangeAttribute>()) != null ) { item = new TextMenu.Slider(name, i => i.ToString(), attribRange.Min, attribRange.Max, (int)value) .Change(v => prop.SetValue(settings, v)) .NeedsRelaunch(needsRelaunch) ; } if (item == null) { continue; } menu.Add(item); } }