跳转至

Session, Settings, SaveData

这部分内容在 Everest Wiki 里是在配置环境的同时说的, 我个人认为应该推后一点但是似乎现在推后的有点过头.

你应该还记得我们最开始的那个继承于 EverestModule 的类 MyCelesteModModule, 到目前为止我们只使用了它的 Load 方法和 Unload 方法用于加载初始化我们的钩子. 当然, 这肯定是不止的, 那么这一节将会聚焦于 EverestModule 的更多的内容.

Settings

基本

Settings, 顾名思义就是选项的意思. Everest 为我们封装了一个非常便利的系统, 我们要做的只是简单地加入属性, 然后装饰几个简单的特性, 就能创建出易用的菜单选项, 剩下的 ui 方面和保存读取的工作都由 Everest 负责.

在此之前, 我们先保存一下我们的 EverestModule 实例以方便我们访问它的实例(Everest 会确保它是单例的):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class MyCelesteModModule : EverestModule
{
    public static MyCelesteModModule Instance { get; private set; }

    public override void Load()
    {
        Instance = this;
    }

    public override void Unload()
    {
    }
}

然后新建一个名字最好以 mod 开头, Settings 结尾的 MyCelesteModSettings 类并继承 EverestModuleSettings:

1
2
3
4
public class MyCelesteModSettings : EverestModuleSettings
{

}

然后在 module 类里这样注册这个类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class MyCelesteModModule : EverestModule
{
    public static MyCelesteModModule Instance { get; private set; }

    public override Type SettingsType => typeof(MyCelesteModSettings);
    public static MyCelesteModSettings Settings => (MyCelesteModSettings)Instance._Settings;

    public override void Load()
    {
        Instance = this;
    }

    public override void Unload()
    {
    }
}

现在, 我们向选项中新加一条公开的 bool 类型的属性:

1
2
3
4
public class MyCelesteModSettings : EverestModuleSettings
{
    public bool AnInterestingSwitch { get; set; }
}

现在直接编译你的项目, 然后你应该会看到 Everest 为你在 mod 选项中生成了这样一条有趣的设置:

interesting-switch

随后你就能在你 mod 的任何地方用 MyCelesteModSettings.Settings.AnInterestingSwitch 来访问这个开关了.

除此之外 Everest 还支持枚举, 字符串和数字, 它们分别会生成这样的选项:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class MyCelesteModSettings : EverestModuleSettings
{
    public bool AnInterestingSwitch { get; set; }

    [SettingRange(0, 100)]
    public int AnInteger { get; set; }

    public string AString { get; set; }

    public DayOfWeek DayOfWeek { get; set; }
}

more-options

Info

AString 那条选项是一个按钮, 按下后会进入文字输入界面, 冒号后面会跟玩家输入了的文字.
DayOfWeek 选项允许你左右选择 DayOfWeek 枚举中的每一项.

其中 int 那条属性需要装饰 SettingRange 特性并指定最大最小值, 否则 Everest 会拒绝生成它, 此外它还有个参数可以用来指定该选项是否需要 "大的范围" (LargeRange), 如果指定为 true, 则长按调整该值时的增长速度会慢慢增加以便快速调整至更大/小的值.

除了 SettingRange 特性, 还有更多其他的特性用于指示该选项的一些性质:

  • SettingNeedsRelauch: 指示该选项需要重启游戏生效.
  • SettingNumberInput: 如果该选项对应的属性是个数字(int, float), 那么使用输入文字的菜单来输入数字而不是左右切换数字.
    • allowNegatives: 是否允许负数
    • maxLength: 字符串最大长度
  • SettingMaxLength: 设置该字符串选项的最大长度. 否则默认 12 个字符.
    • max: 最大长度
  • SettingInGame: 指示该选项是否出现在游戏内(图内)菜单中.
    • inGame: 如上.
  • SettingIgnore: 指示是否忽略该选项被加入菜单, 注意忽略后依然会被 Everest 正常读取保存.
  • YamlIgnore: 指示是否忽略该属性的保存.

Note

YamlIgnore 位于命名空间 YamlDotNet.Serialization 内.


本地化

你可能会发现默认生成的选项名字就是属性名, 尽管我们通常能猜到它的意思以及它会干什么, 但是玩家们通常不会, 所以我们还得为其配置本地化, 其就包含了我们通常讲的 "汉化", "中文翻译".

在这里, 我们需要的本地化键名是 modoptions_{类名}_{属性名}, 例如如下类:

1
2
3
4
public class MyCelesteModSettings : EverestModuleSettings
{
    public bool EnableFunnyThing { get; set; }
}

其中那条属性对应的本地化键名则是: modoptions_mycelestemod_enablefunnything, 所以我们将其写在 Dialog 文件里:

ModFolder/Dialog/Simplified Chinese.txt
1
modoptions_mycelestemod_enablefunnything=开启有趣的东西
ModFolder/Dialog/english.txt
1
modoptions_mycelestemod_enablefunnything=Enable funny thing

Note

如果你不知道 dialog 文件是什么的话, 你可以询问 mapper 们, 或者在这里你就干脆照做, 也就是新建如其标题所展示的文件然后粘贴对应内容.

此外, mod 选项的大标题也有个本地化键名, 相对于上面的键名只是将属性名换成了 title, 例如以下 dialog 文件:

ModFolder/Dialog/Simplified Chinese.txt
1
2
modoptions_mycelestemod_enablefunnything=开启有趣的东西
modoptions_mycelestemod_title=我的有趣 mod 的设置 (MyCelesteMod)
ModFolder/Dialog/english.txt
1
2
modoptions_mycelestemod_enablefunnything=Enable funny thing
modoptions_mycelestemod_title=funny MyCelesteMod

Note

时刻记得填充 english.txt, 因为如果其他语言没找到这个键名会默认回退到 english, 如果再没有的话就会直接展示丑陋的键名.

dialog

本地化键名并不是固定的, 你可以使用 SettingName 装饰到类上或者属性上来修改它, 不过我个人不是很建议修改它, 因为默认的值作为键名完全够用, 此外还有一个 SettingSubText 特性, 它可以向该选项被选中时在底下显示一行小字, 它的参数同样是个本地化键名, 类似的还有 SettingSubHeader 特性, 它会向该选项之前加入一个小标题.

对于枚举每一项的值可以使用 modoptions_{类名}_{属性名}_{枚举项名} 来指定, 例如对于 DayOfWeek 枚举类型的 Day 属性:

ModFolder/Dialog/Simplified Chinese.txt
1
2
3
4
5
6
7
modoptions_mycelestemod_day_sunday=星期日
modoptions_mycelestemod_day_monday=星期一
modoptions_mycelestemod_day_tuesday=星期二
modoptions_mycelestemod_day_wednesday=星期三
modoptions_mycelestemod_day_thursday=星期四
modoptions_mycelestemod_day_friday=星期五
modoptions_mycelestemod_day_saturday=星期六

Info

如果你实际测试的话你会发现 字和 字没有渲染出来, 这是正常的, 因为蔚蓝的字库中没有这两个字, 这里我就不赘述如何解决这个问题了, 具体可以咨询 mapper 们. (日常偷懒.jpg)

最后

或许你也已经猜到了, Everest 正是使用的 Yaml 来序列化/反序列化你的 Settings 类, 所以请务必不要在你的 Settings 类中放置奇怪的类和结构体! 如果你要这么做请确保 Everest 能正确地序列化/反序列化你的 Settings 类, 否则你的设置将不会被正常保存, 永远都是默认值.

Session

基本

Session 是一个蔚蓝中保存数据的概念, 它用于保存 "保存并退出" 按钮按下后所保存的数据, 例如当前已激活的 Flag, 重生点, 已拾取的钥匙, 草莓, 核心的模式等. 在自定义你自己的 Session 后, 你可以在这里保存你自己独有的收集物信息, 或者是一些实体需要临时保存于关卡的数据.
与 Settings 相同, 我们需要先创建一个继承于 EverestModuleSession 的类, 然后在模块类中声明它:

MyCelesteModSession.cs
1
2
3
4
5
namespace Celeste.Mod.MyCelesteMod;

public class MyCelesteModSession : EverestModuleSession
{
}
MyCelesteModModule.cs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
namespace Celeste.Mod.MyCelesteMod;

public class MyCelesteModModule : EverestModule
{
    public static MyCelesteModModule Instance { get; private set; }

    public override Type SettingsType => typeof(MyCelesteModSettings);
    public static MyCelesteModSettings Settings => (MyCelesteModSettings)Instance._Settings;

    public override Type SessionType => typeof(MyCelesteModSession);
    public static MyCelesteModSession Session => (MyCelesteModSession)Instance._Session;

    public override void Load()
    {
        Instance = this;
    }

    public override void Unload()
    {
    }
}

然后我们就可以在任何游戏处于关卡内的时候使用 MyCelesteModModule.Session 来访问我们的 Session 了. 如果你尝试在游戏不在关卡内时读取它, 那么你会得到一个 null 值.

假设我们碰了 SetPassByRefillDashesTrigger 之后又碰到 ChangeRespawnTrigger 改变了重生点, 用 Save And Quit 退出了游戏, 再次进入时你会发现重生点是新的, 而 PassByRefill 的冲刺数又变回 1 了, 所以接下来我们尝试通过 Session 来修正这个错误.

MyCelesteModSession.cs
1
2
3
4
5
6
namespace Celeste.Mod.MyCelesteMod;

public class MyCelesteModSession : EverestModuleSession
{
    public Dictionary<string, int> RoomIdToPassByRefillDashes = new();  // 我们将记录每个房间名对应的PassByRefill的冲刺数
}
SetPassByRefillDashesTrigger.cs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
using Celeste.Mod.Entities;

namespace MyCelesteMod;

[CustomEntity("MyCelesteMod/SetPassByRefillDashesTrigger")]
public class SetPassByRefillDashesTrigger : Trigger
{
    public int Dashes;

    public SetPassByRefillDashesTrigger(EntityData data, Vector2 offset)
        : base(data, offset)
    {
        Dashes = data.Int("dashes");
    }

    public override void OnEnter(Player player)
    {
        base.OnEnter(player);
        MyCelesteModModule.Session.RoomIdToPassByRefillDashes[SceneAs<Level>().Session.LevelData.Name] = Dashes;
    }
}
PassByRefill.cs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
using Celeste.Mod.Entities;

namespace MyCelesteMod;

[CustomEntity("MyCelesteMod/PassByRefill")]
public class PassByRefill : Entity
{
    private int _dashes = 1;

    public int Dashes => MyCelesteModModule.Session.RoomIdToPassByRefillDashes.GetValueOrDefault(SceneAs<Level>().Session.LevelData.Name, _dashes);

    private Image image;

    public PassByRefill(Vector2 position, int dashes)
    {
        Depth = 1;
        Dashes = dashes;
        Position = position;
        Hitbox hitbox = new(64, 64);
        Collider = hitbox;

        MTexture tex = GFX.Game["MyCelesteMod/pass_by_refill"];
        image = new(tex);
        Add(image);
    }

    public PassByRefill(EntityData data, Vector2 offset)
        : this(data.Position + offset, data.Int("dashes"))
    {
    }

    public override void Update()
    {
        base.Update();
        var player = Scene.Tracker.GetEntity<Player>();
        if (player is not null && CollideCheck(player))
        {
            player.Dashes = Dashes;
        }
    }
}

SaveData

顾名思义, SaveData 用来保存一些持久化数据, 例如当前存档总时间, 总死亡数, 总草莓数等. 使用它与使用 Session 极其相似:

MyCelesteModSaveData.cs
1
2
3
4
5
namespace Celeste.Mod.MyCelesteMod;

public class MyCelesteModSaveData : EverestModuleSaveData
{
}
MyCelesteModModule.cs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
namespace Celeste.Mod.MyCelesteMod;

public class MyCelesteModModule : EverestModule
{
    public static MyCelesteModModule Instance { get; private set; }

    public override Type SettingsType => typeof(MyCelesteModSettings);
    public static MyCelesteModSettings Settings => (MyCelesteModSettings)Instance._Settings;

    public override Type SessionType => typeof(MyCelesteModSession);
    public static MyCelesteModSession Session => (MyCelesteModSession)Instance._Session;

    public override Type SaveDataType => typeof(MyCelesteModSaveData);
    public static MyCelesteModSaveData SaveData => (MyCelesteModSaveData)Instance._SaveData;

    public override void Load()
    {
        Instance = this;
    }

    public override void Unload()
    {
    }
}