简单数据库框架

简单数据库框架

一个用于redscript和lua的反应式、高性能对象数据库。数据具有存档感知能力,并以可读的JSON格式存储。加载存档时自动载入并持久化。订阅数据库更改以实现实时数据处理

简单数据库框架

overview

一个面向 redscript 和 lua 的响应式高性能对象数据库。数据与存档文件感知,并以可读的 JSON 格式存储。在加载存档时自动加载和持久化。订阅数据库变更,即可在脚本框架间实现实时数据处理。

TL;DR

    let dto: ref<MyDataObject> = new MyDataObject();
    dto.id = "dtokey";
    dto.value = "dtovalue";
    dto.name = "My DTO";
    dto.type = "SimpleDatabaseFramework.Example.MyDataObject";
    dto.Save();
// 保存游戏并退出
...
// 启动游戏并加载存档
let dto: DataObject = player.GetObject("dtokey");
FTLog(dto.name+":"+dto.value);
...
My DTO:dtovalue

该框架包含两种主要的响应式数据机制数据(瞬态)和对象(持久化)。您可能需要为您的模组提供临时状态感知(事件感知),但这些状态不会跨存档或重启保留。这被称为瞬态数据 API。对于随存档文件存储的永久数据,该框架提供持久化对象行为。两者将在下方的『用法』部分详细讨论。

值得一提的是,数据 API 不使用类或类型,因此更适合动态数据共享。您也可以将对象 API 用于临时数据,只需不保存对象即可——如果您更倾向于在编码中使用对象类和 DTO。选择权在您手中。

可能的使用场景

该框架的一些可能使用场景包括:

  1. 任务数据——您有想要存储的自定义任务数据,以便在玩家存档加载时重新加载。
  2. 配置数据——您想为您的模组存储各种用户配置或设置,并使其跨存档保持持久化。同时希望各种 lua/redscript 脚本能立即收到任何配置更改的通知,以便做出响应。
  3. 刷怪系统状态——您有一个动态刷怪系统,需要将刷怪属性存储在瞬态查找数据库中,每次游戏加载时重置。但同时,当某些数据发生变化时(例如,NPC 执行操作并更新其数据),所有监听脚本都需要立即收到通知。
  4. 动态载具或 NPC 遥测——您需要存储载具状态或 NPC 行为的动态数据,并在其更改时立即通知各种脚本。

重要!这是一个*早期访问版本,因为我计划在未来的简单任务框架模组中使用该框架(和我的其他简单框架),该模组将利用所有其他简单框架。*

install

可通过模组管理器安装,或解压到游戏目录。

usage

LUA

以下是创建持久化对象并将其与存档文件一起存储的方法。稍后重新加载该特定存档,将使对象可用并保持其先前的状态。

> json = Game.GetPlayer():CreateObject()
> json:SetKeyString("id","newid")
> json:SetKeyString("value","newvalue")
> json = Game.GetPlayer():SetJsonObject(json)

注意:数据存储在位于 r6\storages\SimpleDatabaseFramework.json 文件中。

稍后(或许在重新加载后)检索该对象。状态锁定到存档文件,因此在一个存档中更改同一对象不会跨存档传播。

> json = Game.GetPlayer:GetJsonObject("newid")
> print(json.GetKeyString("value"))
newvalue

存储临时数据

> Game.GetPlayer():SetValue("akey",{"value1","value2","value3"})

获取临时数据

> values = Game.GetPlayer():GetValue("akey")
> for value in values do print(value) end

数据状态监听器(瞬态)

-- 创建数据处理器
function datahandler(tags)
  for i, tag in ipairs(tags) do
    print("[SimpleDatabaseUpdateObjectMessage EXAMPLE] Object ID ",tag.value)
    for i, v in ipairs(Game.GetPlayer():GetValue(tag.value)) do
       print("[SimpleDatabaseUpdateObjectMessage EXAMPLE][",tag.value,"] Value ",v)
    end
  end
end

-- 添加您的数据处理器
eventFramework = GetMod("SimpleEventFramework")
eventFramework:AddEventMessageListener("BaseStatusEffect.SimpleDatabaseUpdateDataMessage", datahandler)

对象状态监听器(持久化)

 -- 创建对象处理器
function handler(tags)
  for i, tag in ipairs(tags) do
    print("[SimpleDatabaseUpdateObjectMessage EXAMPLE] Object ID ",tag.value)

    -- 获取已更新的对象
    local json = Game.GetPlayer():GetObject(tag.value)
    print("[SimpleDatabaseUpdateObjectMessage EXAMPLE] json ID ", json:GetKeyString("id"))

    local type = json:GetKeyString("type")
    print("[SimpleDatabaseUpdateObjectMessage EXAMPLE]  json Type ",type)
    if type == "SimpleDatabaseFramework.Example.MyDataObject" then
      print("[SimpleDatabaseUpdateObjectMessage EXAMPLE] json Value ",json:GetKeyString("value"))
    end
    -- 使用 DTO 字段做其他事情,例如 json:GetKeyString("somekey")
    -- 实际键字符串将取决于您的对象
  end
end

-- 添加您的对象处理器
eventFramework = GetMod("SimpleEventFramework")
eventFramework:AddEventMessageListener("BaseStatusEffect.SimpleDatabaseUpdateObjectMessage", handler)

REDSCRIPT

定义一个自定义数据传输对象(DTO)

public class MyDataObject extends DataObject {
    public let myproperty: String;
public let anotherprop: Int32;
}

数据状态监听器。当有临时数据更新时,将调用 Call() 方法,并且更新中的所有数据标签都可在 this.tags 中使用,如下所示。您无需手动发送数据,引擎会自动处理。

import SimpleDatabaseFramework.System.*
import SimpleEventFramework.System.*

public class MyDataListenerClass extends ListenerCallback {
  public func Call() -> Void {
    for tag in this.tags {
        FTLog("[MyDataListenerClass] tag "+this.event+":"+NameToString(tag));
    }
    StatusEffectHelper.RemoveStatusEffect(this.player, t"BaseStatusEffect.SimpleDatabaseUpdateDataMessage");
  }
}

@wrapMethod(PlayerPuppet)
protected cb func OnGameAttached() -> Bool {
    let ret: Bool  = wrappedMethod();

    // 添加数据监听器
    let eventFramework = SimpleEventFramework.GetInstance(this);
    if IsDefined(eventFramework) {
        let listener: ref<MyDataListenerClass> = new MyDataListenerClass();
        listener.event = "BaseStatusEffect.SimpleDatabaseUpdateDataMessage";
        eventFramework.AddEventMessageListener(listener);
    }
}

对象状态监听器

import SimpleDatabaseFramework.System.*
import SimpleEventFramework.System.*

public class MyObjectListenerClass extends DatabaseListenerCallback {
  public func Updated(dto: ref<DataObject>) -> Void {
    if dto.IsA(n"SimpleDatabaseFramework.Example.MyDataObject") {
      let mydto: ref<MyDataObject> = dto as MyDataObject;
      FTLog("[SIMPLE DATABASE FRAMEWORK][MyObjectListenerClass] Updated dto "+mydto.value);
    }
  }
  public func Removed(dto: ref<DataObject>) -> Void {
    if dto.IsA(n"SimpleDatabaseFramework.Example.MyDataObject") {
      let mydto: ref<MyDataObject> = dto as MyDataObject;
      FTLog("[SIMPLE DATABASE FRAMEWORK][MyObjectListenerClass] Removed dto "+mydto.value);
    }
  }
}

public class MyDataObject extends DataObject {
    public let value: String;
}

@wrapMethod(PlayerPuppet)
protected cb func OnGameAttached() -> Bool {
    let ret: Bool  = wrappedMethod();

    // 添加对象监听器
    let databaseFramework = SimpleDatabaseFramework.GetInstance(this);

    if IsDefined(databaseFramework) {
        let listener: ref<MyObjectListenerClass> = new MyObjectListenerClass();
        databaseFramework.AddListener(listener);
    }
    return ret;
}

创建并保存对象。当对象被保存时,所有监听器都会被通知,包括 redscript 和 lua 中的对象和数据监听器。

@addMethod(PlayerPuppet)
public func ExampleDTO() -> Void {
    FTLog("[SIMPLE DATABASE FRAMEWORK] ExampleDTO");

    let dto: ref<MyDataObject> = new MyDataObject();
    dto.id = "dtokey";
    dto.value = "dtovalue";
    dto.name = "My DTO";
    dto.type = "SimpleDatabaseFramework.Example.MyDataObject";
    dto.Save();
}

对于对象监听器,当任何对象被保存(即更新)时,监听器的 Updated() 方法将被调用,并传递已更新的 DTO 对象。

注意: 您需要将接收到的 DataObject 转换为您为模组创建的任何特定子类类型,如下所示。

public class MyObjectListenerClass extends DatabaseListenerCallback {
  public func Updated(dto: ref<DataObject>) -> Void {
    if dto.IsA(n"SimpleDatabaseFramework.Example.MyDataObject") {
      let mydto: ref<MyDataObject> = dto as MyDataObject;
      FTLog("[SIMPLE DATABASE FRAMEWORK][MyObjectListenerClass] Updated dto "+mydto.value);
    }
  }
...
}

此外,系统还会发出一个数据事件,发送已更新 DTO 的 id。然而,这种方法主要是为了支持 lua 监听器,因为 redscript 类类型对 lua 监听器不可用。这些监听器将根据对象 id 检索对象的 Json 版本。

this.simpleEventFramwork.SendEvent("BaseStatusEffect.SimpleDatabaseUpdateObjectMessage", [dto.id]);

请参阅上方的对象状态监听器示例,了解如何在 lua 中处理这些事件。

examples

此处显示的所有示例都可以在 [u]Cyberpunk 2077\r6\scripts\SimpleDataFramework\Example.reds[/u] 文件中找到并运行。

对于 lua,您可以使用以下命令初始化示例代码

> sdf = GetMod("SimpleDatabaseFramework")
> sdf:Example()
function SimpleDatabaseFramework:Example()
  eventFramework = GetMod("SimpleEventFramework")
  eventFramework:AddEventMessageListener("BaseStatusEffect.SimpleDatabaseUpdateObjectMessage", handler)
  eventFramework = GetMod("SimpleEventFramework")
  eventFramework:AddEventMessageListener("BaseStatusEffect.SimpleDatabaseUpdateDataMessage", datahandler)
end

并参考 [u]Cyberpunk 2077\bin\x64\plugins\cyber_engine_tweaks\mods\SimpleDatabaseFramework\init.lua[/u] 中的代码。

查看我的其他简单框架

Simple Framework 1Simple Framework 2Simple Framework 3

docs

SimpleDatabaseFramework API 文档

SimpleDatabaseFramework 模组提供了一个简单的 API,用于跨游戏会话存储和检索持久化数据。它提供了一个简单的键值存储(用于字符串数组)和一个更复杂的对象存储(将数据序列化为 JSON 文件)。

概述

该框架旨在作为其他模组的集中式数据库,减少每个模组自行实现文件 I/O 和数据持久化逻辑的需求。它主要通过添加到 PlayerPuppet 类的方法来公开其 API,以便使用。

有两种主要的数据存储方式:

  • 作为简单的键值对,其中值是字符串数组。
  • 作为复杂的 DataObject 实例,可序列化为 JSON 并从 JSON 反序列化。

快速入门

要使用该框架,您可以直接在 PlayerPuppet 实例上调用 API 方法。该框架是一个自动初始化的可脚本化系统。

PlayerPuppet API

这些方法可以在任何 PlayerPuppet 实例上直接调用(例如,通过 Game.GetPlayer() 获取的本地玩家)。

键值存储

SetValue(key, values) -> Void

存储与唯一键关联的字符串数组。如果键已存在,其值将被覆盖。

  • key: 数据的唯一字符串标识符。
  • values: 要存储的字符串数组。

GetValue(key) -> array

检索给定键的字符串数组。

  • key: 要检索的数据的键。

RemoveValue(key) -> Void

从数据库中移除一个键及其关联的字符串数组。

  • key: 要移除的数据的键。

对象存储

SetObject(dto) -> Void

保存或更新扩展 DataObject 的自定义数据对象。对象的 id 字段用作唯一键。

  • dto: 要保存的数据对象。

GetObject(key) -> ref

按 ID 检索自定义数据对象。您需要将返回的 DataObject 转换为您的特定自定义类型。

  • key: 要检索的对象的唯一 id

RemoveObject(key) -> Void

使用其键从数据库中移除自定义数据对象。

  • key: 要移除的对象的唯一 id

底层 JSON API

这些函数适用于需要直接操作 JsonObject 的更高级用途。

CreateObject() -> ref

创建并返回一个新的、空的 JsonObject

SetJsonObject(json) -> Void

在数据库中存储一个 JsonObject。该对象必须有一个名为 "id" 的字符串字段,该字段将用作键。

GetJsonObject(key) -> ref

按键从数据库中检索一个 JsonObject

创建自定义数据对象

要存储复杂数据,您需要定义一个扩展 DataObject 的类。该基类提供了基本的 idtypename 字段,以及一个 Save() 方法。

public class MyDataObject extends DataObject {
    public let myCustomString: String;
    public let myCustomNumber: Int32;
}

要保存此对象的实例:

let myDto: ref<MyDataObject> = new MyDataObject();
myDto.id = "unique_identifier_for_my_object";
myDto.type = "MyMod.MyDataObject"; // 存储类型是一个好习惯
myDto.name = "My First Object";
myDto.myCustomString = "Hello World!";
myDto.myCustomNumber = 123;

// 这会在内部调用 player.SetObject(myDto)
myDto.Save();

监听数据库变更

您可以通过创建一个扩展 DatabaseListenerCallback 的监听器类,来监听数据库中对象的更新和移除。

1. 定义您的监听器

创建一个扩展 DatabaseListenerCallback 的类,并实现 UpdatedRemoved 函数。

public class MyObjectListener extends DatabaseListenerCallback {

  public func Updated(dto: ref<DataObject>) -> Void {
    // 检查更新的对象是否是您感兴趣的
    if dto.IsA(n"MyMod.MyDataObject") {
      let mydto: ref<MyDataObject> = dto as MyDataObject;
      Log("MyDataObject with id " + mydto.id + " was updated!");
    }
  }

  public func Removed(dto: ref<DataObject>) -> Void {
    if dto.IsA(n"MyMod.MyDataObject") {
      let mydto: ref<MyDataObject> = dto as MyDataObject;
      Log("MyDataObject with id " + mydto.id + " was removed!");
    }
  }
}

2. 注册您的监听器

在您的 OnGameAttached 或其他初始化逻辑中,获取 SimpleDatabaseFramework 实例并添加您的监听器。

@wrapMethod(PlayerPuppet)
protected cb func OnGameAttached() -> Bool {
    let ret: Bool  = wrappedMethod();

    let databaseFramework = SimpleDatabaseFramework.GetInstance(this);
    if IsDefined(databaseFramework) {
        let listener: ref<MyObjectListener> = new MyObjectListener();
        databaseFramework.AddListener(listener);
    }

    return ret;
}

示例用法

以下是一个完整的示例,说明如何定义、保存、检索和监听自定义数据对象的更改。

module MyMod.Example

import SimpleDatabaseFramework.System.*

// 1. 定义您的自定义数据对象
public class MySettingsObject extends DataObject {
    public let volume: Float;
    public let enabled: Bool;
}

// 2. 定义一个变更监听器
public class MySettingsListener extends DatabaseListenerCallback {
  public func Updated(dto: ref<DataObject>) -> Void {
    if dto.IsA(n"MyMod.Example.MySettingsObject") {
      let settings: ref<MySettingsObject> = dto as MySettingsObject;
      Log("Settings updated! New volume: " + settings.volume);
    }
  }

  public func Removed(dto: ref<DataObject>) -> Void {
    // 如有必要,处理移除情况
  }
}

@addMethod(PlayerPuppet)
public func SaveMySettings(volume: Float, enabled: Bool) -> Void {
    let settings: ref<MySettingsObject> = new MySettingsObject();
    settings.id = "my_mod_settings";
    settings.type = "MyMod.Example.MySettingsObject";
    settings.name = "My Mod Settings";
    settings.volume = volume;
    settings.enabled = enabled;
    settings.Save(); // 这将触发我们监听器中的 'Updated' 函数
}

@addMethod(PlayerPuppet)
public func LoadMySettings() -> ref<MySettingsObject> {
    let dto = this.GetObject("my_mod_settings");
    if IsDefined(dto) {
        return dto as MySettingsObject;
    }
    return null;
}

// 3. 在游戏启动时注册监听器
@wrapMethod(PlayerPuppet)
protected cb func OnGameAttached() -> Bool {
    wrappedMethod();

    let databaseFramework = SimpleDatabaseFramework.GetInstance(this);
    if IsDefined(databaseFramework) {
        databaseFramework.AddListener(new MySettingsListener());
    }
    return true;
}