外观
目录
简介
Celeste 通过三种方式存储持久化数据。
Session: 包含与当前游玩进程相关的数据,例如死亡次数、花费时间、当前收集的草莓数以及当前房间。
会话数据在 保存并退出 之后仍会保留,并在 重开章节 或 返回地图 时重置。SaveData: 包含与整个存档文件相关的数据,例如冲刺总次数、最少死亡数、历史收集的草莓总数,以及是否已收集 水晶之心。
存档数据在整个存档文件中持久保留,只有在删除其关联的存档文件时才会被重置。Settings: 包含全局适用于整个 Celeste 的数据,例如当前语言、音乐和音效音量、默认存档文件名,以及是否已解锁 变体模式。
设置绝不会在游戏内被重置。重置设置的唯一方式是在 Celeste 之外删除设置文件。
Everest 允许模组以 EverestModuleSession、EverestModuleSaveData 和 EverestModuleSettings 类的形式保存持久化数据。
原版信息在写入磁盘时会序列化 🔗为 XML 🔗,而 Everest 默认使用 YAML 🔗。
如果出于任何原因你需要为持久化数据定义自己的格式,你可以从 EverestModuleBinarySession、EverestModuleBinarySaveData 和 EverestModuleBinarySettings 继承。
设置
说明
如果你使用了 Celeste Code Mod 模板,那么所有设置工作都已为你完成。
参阅 代码模组设置 页面了解如何使用该模板。
要能够存储持久化数据,你需要创建一个继承自上述某个 Everest 类型的类型。
以自定义存档数据为例。创建一个继承自 EverestModuleSaveData 的类。
cs
// ExampleModSaveData.cs
namespace Celeste.Mod.ExampleMod;
public class ExampleModSaveData : EverestModuleSaveData
{
}然后,你需要告诉 Everest 存档数据所存储的类型。在你的模组类中,重写 SaveDataType 属性并将其值设为 ExampleModSaveData 类型。
cs
// ExampleModModule.cs
namespace Celeste.Mod.ExampleMod;
public class ExampleModModule : EverestModule
{
public static ExampleModModule Instance;
public override Type SaveDataType => typeof(ExampleModSaveData);
public ExampleModModule()
{
Instance = this;
}
// ...
}EverestModule 通过实例属性 _SaveData 暴露你的存档数据。建议同时创建一个静态的 SaveData 属性,将实例属性转换为 SaveData 类型。
cs
// ExampleModModule.cs
namespace Celeste.Mod.ExampleMod;
public class ExampleModModule : EverestModule
{
public static ExampleModModule Instance;
public override Type SaveDataType => typeof(ExampleModSaveData);
public static ExampleModSaveData SaveData => (ExampleModSaveData)Instance._SaveData;
public ExampleModModule()
{
Instance = this;
}
}用法
存储数据很简单——只需在类上定义一个公共实例属性 🔗,并在需要时对其赋值。请注意,两个访问器都必须是公共的。
其他任何内容都不会被序列化。
提示
如果你想从序列化中省略某个属性,请给它加上 [YamlIgnore] 特性。
这需要添加 YamlDotNet NuGet 包。
让我们定义一些自定义存档数据属性:
cs
// ExampleModSaveData.cs
namespace Celeste.Mod.ExampleMod;
public class ExampleModSaveData : EverestModuleSaveData
{
// Public instance properties work
public bool MyBool { get; set; }
// Default to 10 if there is no saved value
public int MyNumber { get; set; } = 10;
// Don't save the property value
[YamlIgnore]
public float MyFloat { get; set; }
}然后,要访问属性值,请从你的模组类中引用 Session、SaveData 或 Settings 属性并访问所需内容。
在此示例中,ExampleModModule.SaveData.MyNumber 从我们的自定义存档数据中访问 MyNumber 属性。
自定义设置
EverestModuleSettings 类型很特殊。Everest 会检查你的设置类,并在 Mod Options 菜单中生成你模组的设置区块。
它的行为非常可定制。
对于入门设置,你可以使用许多特性来控制设置的行为。
对于高级控制,你可以指定设置属性的菜单项如何实例化,并手动将其添加到菜单中。
如果你觉得自己是设置大师,你可以在你的模组模块中重写整个模组区块的生成方式。
提示
如果你希望某个属性不出现在设置菜单中,请给它加上 [SettingIgnore] 特性。
入门设置
如果你想要简单的设置,Everest 会为你完成大量繁重工作。它会根据属性类型自动猜测要选择哪个菜单项。
你也可以为属性添加特性,告诉 Everest 你希望设置如何表现。
支持的属性类型包括:
此外,你还可以创建子菜单。
Boolean
bool 属性会变成一个开/关滑块。
cs
// Define an on/off toggle
public bool MyToggle { get; set; }Int32 (int)
int 属性可以是滑块,也可以是带用户进入数字输入菜单的按钮。
要使该属性成为滑块,你必须给它加上 [SettingRange] 特性来定义设置的允许范围。
你还可以指定是否为较大范围优化滑块。
要使该属性成为数字输入,你必须给它加上 [SettingNumberInput] 特性来定义最大位数,以及是否允许负数。
默认情况下允许负数,且设置最多可有 6 位数字。
重要
使用 [SettingNumberInput] 的设置将在游戏内被禁用,因为在游戏内尝试打开数字输入菜单会导致 Celeste 崩溃。
cs
// Set the range of MySlider to [-10, 10]
[SettingRange(min: -10, max: 10)]
public int MySlider { get; set; }
// Set the range of MyLargeSlider to [-1000, 1000],
// and optimize for large ranges
[SettingRange(min: -1000, max: 1000, largeRange: true)]
public int MyLargeSlider { get; set; }
// Allow the user to type an up to 6 digits long number
// Note: The setting will be disabled in-game
[SettingNumberInput]
public int MyNumberInput { get; set; }
// Allow the user to type an up to 5 digits long number, without allowing negatives
// Note: The setting will be disabled in-game
[SettingNumberInput(allowNegatives: false, maxLength: 5)]
public int MyOtherNumberInput { get; set; }Enum
Enum 属性会变成一个滑块,可在其所有值之间滚动。
顺序由赋予该枚举的值决定。
cs
public enum MyEnumeration
{
One,
Two,
Three,
Four,
Five
}
// Define a slider which scrolls through the options in MyEnumeration
public MyEnumeration MyEnumSlider { get; set; }
// Scroll order is determined by the enum's integer value
// This means that the slider will scroll right in the order of World, There, Hello
public enum MyOtherEnumeration
{
Hello = 5,
There = 0,
World = -5
}
// Define a slider which scrolls through the options in MyOtherEnumeration
public MyOtherEnumeration MyOtherEnumSlider { get; set; } = MyOtherEnumeration.World;Single (float)
float 属性会变成一个带用户进入数字输入菜单的按钮。
你必须给它加上 [SettingNumberInput] 特性来定义最大位数,以及是否允许负数。
默认情况下允许负数,且设置最多可有 6 位数字。
重要
使用 [SettingNumberInput] 的设置将在游戏内被禁用,因为在游戏内尝试打开数字输入菜单会导致 Celeste 崩溃。
cs
// Allow the user to type an up to 6 digits long number
[SettingNumberInput]
public int MyNumberInput { get; set; }
// Allow the user to type an up to 5 digits long number, without allowing negatives
[SettingNumberInput(allowNegatives: false, maxLength: 5)]
public float MyOtherNumberInput { get; set; }String
string 属性会变成一个带用户进入文本输入菜单的按钮。
你可以使用 [SettingMinLength] 和 [SettingMaxLength] 特性控制字符串长度范围。
默认情况下,字符串可以是 1 到 12 个字符。
重要
字符串设置将在游戏内被禁用,因为在游戏内尝试打开文本输入菜单会导致 Celeste 崩溃。
cs
// Allow the user to type a string between 1 and 12 characters
public string MyTextInput { get; set; }
// Allow the user to type a string exactly 6 characters long
[SettingMinLength(6)]
[SettingMaxLength(6)]
public string MyTextInput { get; set; }ButtonBinding
ButtonBinding 属性允许模组定义自定义按键绑定,与原版的相互独立。
它们可以在模组的 Mod Options 区块中重新绑定。
你可以通过添加 [DefaultButtonBinding] 特性来指定默认绑定。Buttons 枚举使用的是 Xbox 布局。这意味着 Xbox 上的 A 键对应 Switch 上的 B 键、PlayStation 上的 X 键,仅举几例。
cs
// Define a custom button binding
// Defaults to the A button on controller, and the C key on keyboard
[DefaultButtonBinding(button: Buttons.A, key: Keys.C)]
public ButtonBinding MyCustomBinding { get; set; }
// Define another custom button binding
// Defaults to the A, B, X and Y buttons on controller,
// and the Z, X and C keys on keyboard
[DefaultButtonBinding(
buttons: new[] {
Buttons.A, Buttons.B, Buttons.X, Buttons.Y
},
keys: new[] {
Keys.Z, Keys.X, Keys.C
}
)]
public ButtonBinding MyOtherCustomBinding { get; set; }然后,在与绑定交互时,你可以使用 ButtonBinding 的各种成员:
MyCustomBinding.Pressed- 绑定是否刚刚被按下或正处于缓冲中MyCustomBinding.Check- 绑定当前是否被按住MyCustomBinding.Repeating- 绑定是否已按住足够长时间而进入重复状态MyCustomBinding.Released- 绑定是否刚刚被释放MyCustomBinding.ConsumePress()- 消耗按下与缓冲,使绑定从当前帧剩余部分开始不再报告按下MyCustomBinding.ConsumeBuffer()- 仅消耗缓冲,使绑定从当前帧剩余部分开始不再报告按下(仅当输入被缓冲时)
ButtonBinding.ConsumePress() 与 ButtonBinding.ConsumeBuffer() 之间的区别仅在按钮按下被登记的同一帧内调用时才显现。ButtonBinding.ConsumePress() 会使绑定从当前帧剩余部分开始不再报告按下,而 ButtonBinding.ConsumeBuffer() 会继续报告按下直到下一帧。
说明
默认情况下,ButtonBinding 有 0.08 秒 (5 帧) 的缓冲时间。
如果你想更改它 (以及绑定的其他属性),请在模组类中重写 OnInputInitialize 并在其中修改绑定属性。
cs
// ExampleModModule.cs
// Called by Everest in Input.Initialize()
public override void OnInputInitialize()
{
// Remember to call base.OnInputInitialize(),
// so that Everest creates your bindings properly
base.OnInputInitialize();
// Set BufferTime to 0 seconds
Settings.MyCustomBinding.BufferTime = 0;
}子菜单
要创建子菜单,请创建一个带有 [SettingSubMenu] 特性的类。
然后,创建一个以你刚刚定义的类为类型的属性。
说明
适用于设置类的所有限制同样适用于子菜单类。
另请注意,并非所有特性都受支持或能按预期工作。
如果你有修复该问题的想法,Everest 欢迎提交 pull request。
cs
[SettingSubMenu]
public class ExampleSubMenu
{
public bool Toggle { get; set; }
[SettingRange(min: -10, max: 10)]
public int Slider { get; set; }
}
// Create a submenu
// Remember to initialize it to set its default values
public ExampleSubMenu SubMenu { get; set; } = new();通用特性
在上述特性之外,还有一些不特定于属性类型的特性。
这些特性包括:
[SettingName]
允许你为设置定义自定义 Dialog ID。
如果未指定,设置的 Dialog ID 将是 $"modoptions_{typeName}_{propertyName}",其中 typeName 是设置类型名 (如有则去掉结尾的 Settings 部分),propertyName 是属性的名称。
如果 Dialog ID 未定义,设置名称将以空格分隔的 PascalCase 形式显示。
提示
Dialog ID 不区分大小写。
这意味着 "MODOPTIONS_EXAMPLEMOD_ABC" 与 "modoptions_examplemod_abc" 相同。
cs
namespace Celeste.Mod.ExampleMod;
// The default dialog keys will begin with "modoptions_ExampleMod_",
// because the "Settings" part is stripped
public class ExampleModSettings : EverestModuleSettings
{
// Use the default dialog key for this setting,
// which is "modoptions_ExampleMod_UnnamedSetting"
// If there is such a dialog ID defined, use its translation,
// else default to spaced pascal case, which is "Unnamed Setting"
public bool UnnamedSetting { get; set; }
// Use a custom dialog key for this setting
// If there is such a dialog ID defined, use its translation,
// else default to spaced pascal case, which is "Named Setting"
[SettingName("EXAMPLEMOD_SETTINGS_NAMEDSETTING")]
public bool NamedSetting { get; set; }
}此特性也可应用于设置类型,用于定义设置标题头所使用的 Dialog ID。
如果未指定,标题 Dialog ID 将是 $"modoptions_{typeName}_title",其中 typeName 是设置类型名 (如有则去掉结尾的 Settings 部分)。
如果 Dialog ID 未定义,标题头将以空格分隔的 PascalCase 形式显示。
cs
namespace Celeste.Mod.ExampleMod;
// Use a custom dialog key for the Mod Options title header
// If there is such a dialog ID defined, use its translation,
// else default to spaced pascal case, which is "Example Mod",
// because the "Settings" part is stripped
[SettingName("EXAMPLEMOD_SETTINGS_TITLE")]
public class ExampleModSettings : EverestModuleSettings
{
}[SettingSubText]
允许你为设置定义描述,该描述在设置被选中时显示。
Everest 会尝试将特性内容解释为 Dialog ID,如果无法解释,则按原样显示内容。
提示
虽然理论上你可以使用任何文本,但你应该使用 Dialog ID。这可以确保你的描述可被翻译。
cs
// Add a description with the given dialog ID
// If there is such a dialog ID defined, use its translation,
// else display it unchanged
[SettingSubText("MODOPTIONS_EXAMPLEMOD_EXAMPLE_HINT")]
public bool ToggleWithDescription { get; set; }[SettingSubHeader]
允许你用副标题分隔设置。
Everest 会尝试将特性内容解释为 Dialog ID,如果无法解释,则按原样显示内容。
提示
虽然理论上你可以使用任何文本,但你应该使用 Dialog ID。这可以确保你的副标题可被翻译。
cs
// Add a subheader before the setting with the given dialog ID
// If there is such a dialog ID defined, use its translation,
// else display it unchanged
[SettingSubHeader("MODOPTIONS_EXAMPLEMOD_SUBHEADER")]
public bool ToggleWithSubHeader { get; set; }[SettingInGame]
允许你指定设置是仅在游戏内可见,还是仅在主菜单可见。
cs
// Show the setting in-game only
[SettingInGame(true)]
public bool InGameOnlyToggle { get; set; }
// Show the setting in the main menu only
[SettingInGame(false)]
public bool MainMenuOnlyToggle { get; set; }[SettingNeedsRelaunch]
允许你在设置被更改时提醒用户:更改生效需要重启 Celeste。
cs
// Warn when the setting is changed that a restart of Celeste is required
[SettingNeedsRelaunch]
public bool RestartRequiredToggle { get; set; }高级设置
有时你需要比 Everest 默认提供的更多设置控制权。幸运的是,Everest 允许你指定与设置属性对应的菜单项如何创建,而将其余一切交由 Everest 处理。
例如:
- 如果要根据其他设置来禁用某个设置呢?
- 如果要动态创建菜单项呢?
- 如果要创建完全自定义的菜单项呢?
- 如果要创建一个带你进入全屏子菜单的按钮呢?
以上一切皆可实现,下文将对此进行说明。
手动创建菜单项
要指定设置菜单项如何创建,请创建公共实例 CreateXYZEntry 方法,其中 XYZ 是属性的名称。
该方法需要两个参数:
TextMenu menu:应向其添加菜单项的菜单bool inGame:菜单是否在游戏内打开
Everest 会将以下属性类型转换为以下菜单项:
BooleanTextMenu.OnOff
Int32(int)[SettingRange(largeRange: false)]:TextMenu.Slider[SettingRange(largeRange: true)]:TextMenuExt.IntSlider[SettingNumberInput]:TextMenu.Button,其会进入OuiNumberInput
EnumTextMenu.Slider
Single(float)TextMenu.Button,按下时会进入OuiNumberInput
StringTextMenu.Button,按下时会进入OuiModOptionString
ButtonBindingTextMenu.Button,按下时会……- 对于控制器,向场景添加新的
ModuleSettingsButtonConfigUI实体 - 对于键盘,向场景添加新的
ModuleSettingsKeyboardConfigUI实体
- 对于控制器,向场景添加新的
- 子菜单
TextMenuExt.SubMenu
提示
如果你想精确了解 Everest 如何根据代码结构构造你的设置,请查看 EverestModule.CreateModMenuSection 🔗 中那个 庞大的方法。
Everest 大量使用反射 🔗为你构造模组设置。 如果你对 C# 不太熟悉,这段代码可能难以阅读。
以下是针对给定属性类型的相关代码片段:
以下是一个创建 int 属性的示例,该属性:
- 在
40和50之间切换 - 名称来源于
MODOPTIONS_EXAMPLEMOD_INTEGERTOGGLEDialog ID - 仅在游戏内启用
cs
// Create the property; then Everest will look for a CreateIntegerToggleEntry and invoke it
public int IntegerToggle { get; set; } = 40;
// If you need to, you can store the menu entry to edit later.
private TextMenu.OnOff IntegerToggleEntry;
// Specify how to create the menu item
public void CreateIntegerToggleEntry(TextMenu menu, bool inGame)
{
// Create a new TextMenu.OnOff item
// with the MODOPTIONS_EXAMPLEMOD_INTEGERTOGGLE dialog ID
// and make it on if IntegerToggle is 50
menu.Add(IntegerToggleEntry = new TextMenu.OnOff(
label: Dialog.Clean("MODOPTIONS_EXAMPLEMOD_INTEGERTOGGLE"),
on: IntegerToggle == 50
));
// Disable it if not in-game
IntegerToggleEntry.Disabled = !inGame;
// On change, set IntegerToggle to 50 if on, and 40 if off
IntegerToggleEntry.Change(newValue => IntegerToggle = newValue ? 50 : 40);
}说明
CreateXYZEntry 方法也适用于子菜单。
但是,第一个参数会从 TextMenu 变为 TextMenuExt.SubMenu!
cs
public ExampleMenu Menu { get; set; } = new();
[SettingSubMenu]
public class ExampleMenu
{
public int IntegerToggle { get; set; } = 40;
private TextMenu.OnOff IntegerToggleEntry;
// Note that the first parameter changes to a TextMenuExt.SubMenu
public void CreateIntegerToggleEntry(TextMenuExt.SubMenu subMenu, bool inGame)
{
subMenu.Add(IntegerToggleEntry = new TextMenu.OnOff(
label: Dialog.Clean("MODOPTIONS_EXAMPLEMOD_EXAMPLEMENU_INTEGERTOGGLE"),
on: IntegerToggle == 50
));
IntegerToggleEntry.Disabled = !inGame;
IntegerToggleEntry.Change(newValue => IntegerToggle = newValue ? 50 : 40);
}
}运行时创建设置
CreateXYZEntry 并不一定要与它所命名的属性绑定。事实上,每个 CreateXYZEntry 方法并不限于只能创建一个菜单项。
这可用于创建动态设置菜单。
以下是一个示例:一个包含许多动态开/关设置的子菜单。
cs
// The dictionary stores the actual dynamic settings
// Remember to make it a public instance property so that it gets serialized
public Dictionary<string, bool> DynamicSettings { get; set; } = new();
// Don't serialize the dynamic menu - there's nothing there anyway
[YamlIgnore]
public DynamicSettingsMenu SettingsMenu { get; set; } = new();
// Create the actual submenu class
[SettingSubMenu]
public class DynamicSettingsMenu
{
// Create a dummy property so that we can make use of the CreateDummyEntry method
// We won't be actually using this - we just want the method
[YamlIgnore]
public bool Dummy { get; set; }
// If you need to access the setting items in the future, they'll be stored here
public Dictionary<string, TextMenu.OnOff> DynamicSettingItems = new();
// Remember that the first argument becomes a TextMenuExt.SubMenu
public void CreateDummyEntry(TextMenuExt.SubMenu menu, bool inGame)
{
Dictionary<string, bool> dynamicSettings = ExampleModModule.Settings.DynamicSettings;
foreach ((string settingName, bool settingValue) in dynamicSettings)
{
// Note that the setting name won't be translatable
TextMenu.OnOff settingEntry = new(
label: settingName,
on: settingValue
);
settingEntry.Change(newValue => dynamicSettings[settingName] = newValue);
DynamicSettingItems[settingName] = settingEntry;
menu.Add(settingEntry);
}
}
}现在,你可以通过访问 DynamicSettings 字典来添加、删除、读取和写入动态设置。
说明
CreateXYZEntry 方法仅在 Mod Options 菜单即将打开时被调用一次。
这意味着,当 Mod Options 菜单已经打开时添加的动态设置,需要关闭并重新打开菜单才能与其交互。
全屏子菜单(仅主菜单)
要创建全屏子菜单,请创建一个继承自 OuiGenericMenu 并实现 OuiModOptions.ISubmenu 接口的类。
(Oui 是 Overworld User Interface(Overworld 用户界面)的缩写)
然后,要访问该菜单,请调用 OuiGenericMenu.Goto<T>。
它有一个必需参数,即 Action<Overworld>,当从当前子菜单返回父菜单时会调用它。如有必要,可以在 backToParentMenu 字段中访问它。
传递给该方法的任何其他参数都可以从该 Oui 的 parameters 字段访问。
cs
// OuiExampleSubmenu.cs
namespace Celeste.Mod.ExampleMod;
public class OuiExampleSubmenu : OuiGenericMenu, OuiModOptions.ISubmenu
{
// Set the submenu title
// Titles are generally in all uppercase
public override string MenuName => "EXAMPLE SUBMENU";
// Add menu items
// Note the casing - this method is in camelCase
protected override void addOptionsToMenu(TextMenu menu)
{
// The "return to parent Oui" Action<Overworld> is found in the
// "backToParentMenu" field
// Any remaining parameters passed to "OuiGenericMenu.Goto<T>(...)"
// are present in the "parameters" field
TextMenu.OnOff exampleToggle = new(
label: "Example Toggle",
on: false
);
exampleToggle.Change(newValue =>
Logger.Debug(nameof(OuiExampleSubmenu), $"Example Toggle set to {newValue}.")
);
menu.Add(exampleToggle);
}
}然后,你可以在设置中创建一个按钮,按下时访问该子菜单。
cs
// Create a dummy property so that we can make use of the CreateSubmenuExampleEntry method
// We won't be actually using this - we just want the method
[YamlIgnore]
public bool SubmenuExample { get; set; }
public void CreateSubmenuExampleEntry(TextMenu menu, bool inGame)
{
// Only add the button if in the main menu
if (inGame)
return;
TextMenu.Button submenuButton = new("Submenu Example");
// Go to our custom menu
// When exiting, return to the Mod Options menu
submenuButton.Pressed(() =>
OuiGenericMenu.Goto<OuiExampleSubmenu>(
backToParentMenu: overworld => overworld.Goto<OuiModOptions>()
)
);
menu.Add(submenuButton);
}自定义设置项
如果任何内置菜单项不能满足你的需求,你随时可以创建自己的菜单项。
要做到这一点,请创建一个继承自 TextMenu.Item 的类,并重写其属性。
实现完成后,只需将该菜单项添加到 TextMenu 中。
提示
你可以参考现有类,例如 TextMenu.Button 或 TextMenu.Slider<T>。
下面是一个示例菜单项,当按下 Confirm、Left 或 Right 中的任意绑定键时会播放声音。
cs
// ExampleMenuItem.cs
public class ExampleMenuItem : TextMenu.Item
{
public string Label;
public ExampleMenuItem(string label)
=> Label = label;
// Menu item properties
public override float LeftWidth()
=> ActiveFont.Measure(Label).X;
public override float Height()
=> ActiveFont.LineHeight;
// Mod Search support
public override string SearchLabel()
=> Label;
// Interactions
public override void ConfirmPressed()
=> PlaySound();
public override void LeftPressed()
=> PlaySound();
public override void RightPressed()
=> PlaySound();
private static void PlaySound()
=> Audio.Play(SFX.ui_game_increment_strawberry);
// Rendering
public override void Render(Vector2 position, bool highlighted)
{
float alpha = Container.Alpha;
bool isTwoColumn = Container.InnerContent == TextMenu.InnerContentMode.TwoColumn;
ActiveFont.DrawOutline(
Label,
position: position + (isTwoColumn
? Vector2.Zero
: Vector2.UnitX * (Container.Width / 2f)),
justify: isTwoColumn
? Vector2.UnitY / 2f
: Vector2.One / 2f,
scale: Vector2.One,
color: Disabled
? Color.DarkSlateGray
: (highlighted ? Container.HighlightColor : Color.White) * alpha,
stroke: 2f,
strokeColor: Color.Black * (alpha * alpha * alpha)
);
}
}大师级设置
如果你觉得自己足够独立,想完全由自己处理模组选项,你可以在模组模块中定义 CreateModMenuSection 方法。
重要
因为你重写了该方法,Everest 将不再处理菜单创建。
这意味着前述所有特性以及 CreateXYZEntry 方法都将失效。
cs
// ExampleModModule.cs
namespace Celeste.Mod.ExampleMod;
public class ExampleModModule : EverestModule
{
public static ExampleModModule Instance;
public override Type SessionType => typeof(ExampleModSession);
public static ExampleModSession Session => (ExampleModSession)Instance._Session;
public override Type SaveDataType => typeof(ExampleModSaveData);
public static ExampleModSaveData SaveData => (ExampleModSaveData)Instance._SaveData;
public override Type SettingsType => typeof(ExampleModSettings);
public static ExampleModSettings Settings => (ExampleModSettings)Instance._Settings;
public ExampleModModule()
{
Instance = this;
}
// Override how the mod menu section is created
// The pauseSnapshot argument represents the Level.PauseSnapshot,
// which lets you change how the sound is muffled
// (for example, when hovering over the Music/SFX sliders in vanilla)
protected override void CreateModMenuSection(TextMenu menu, bool inGame, EventInstance pauseSnapshot)
{
// Remember to add the section header, else your settings won't be visible
CreateModMenuSectionHeader(menu, inGame, pauseSnapshot);
// Now, add your own stuff!
// Add your keyboard/controller binding buttons, if necessary
CreateModMenuSectionKeyBindings(menu, inGame, pauseSnapshot);
}
}说明
如果你还需要控制 Mod Options 区块的创建方式,或控制打开按键绑定菜单的按钮的创建方式,你也可以重写 CreateModMenuSectionHeader 和 CreateModMenuSectionKeyBindings 方法。
cs
// ExampleModModule.cs
namespace Celeste.Mod.ExampleMod;
public class ExampleModModule : EverestModule
{
public static ExampleModModule Instance;
public override Type SessionType => typeof(ExampleModSession);
public static ExampleModSession Session => (ExampleModSession)Instance._Session;
public override Type SaveDataType => typeof(ExampleModSaveData);
public static ExampleModSaveData SaveData => (ExampleModSaveData)Instance._SaveData;
public override Type SettingsType => typeof(ExampleModSettings);
public static ExampleModSettings Settings => (ExampleModSettings)Instance._Settings;
public ExampleModModule()
{
Instance = this;
}
protected override void CreateModMenuSectionHeader(TextMenu menu, bool inGame, EventInstance pauseSnapshot)
{
// Create your Mod Options section header
}
protected override void CreateModMenuSection(TextMenu menu, bool inGame, EventInstance pauseSnapshot)
{
// Create your Mod Options settings
// Make sure to create the header first!
}
protected override void CreateModMenuSectionKeyBindings(TextMenu menu, bool inGame, EventInstance pauseSnapshot)
{
// Create your Mod Options section key bindings buttons
// (the "Keyboard Config" / "Controller Config" buttons)
}
}