GamingTS

Meta 类型重写

Meta 重写 (meta rewriting) 是 GTS 中最精妙的机制之一。它允许属性链在编译时(通过 TypeScript 类型)追踪累积的状态,为后续属性提供类型安全的约束。

为什么需要 Meta

考虑以下场景:一个实体先定义变量 chakra,之后在事件处理中需要引用它。如果 chakra 变量名没有在类型层面被追踪,后续 getVariable("chakra") 将无法得到类型检查:

define status {
  id 114072 as ChakraDesiderataStatus;
  variable chakra, 0;     // ← 定义
  on useSkill {
    :getVariable("chakra"); // ← 引用 —— 需要确保 "chakra" 是合法的
  }
}

Meta 重写使 TypeScript 能在编译时验证变量名的合法性。

Meta 的生命周期

每个 ViewModel 都有一个关联的 Meta 类型。初始 Meta 在 defineViewModel 的第三个参数中定义:

const StatusVM = defineViewModel(
  StatusBuilder,
  (helper) => ({ /* 属性定义 */ }),
  null! as { varNames: never },  // 初始 Meta
);

当属性被解析时,Meta 类型可以被子属性"重写"(更新):

// 初始 meta: { varNames: never }

// 解析 variable chakra, 0; 后
// meta 变为: { varNames: "chakra" }

// 解析 variable count, 0; 后
// meta 变为: { varNames: "chakra" | "count" }

缩窄派生 ViewModel 的初始 Meta

当派生 ViewModel 需要使用更具体的初始 Meta 时,调用 .narrow(meta)

type EntityMeta = {
  type: "status" | "summon";
};

class EntityVM extends defineViewModel(
  EntityBuilder,
  (helper) => ({ /* 属性定义 */ }),
  {} as EntityMeta,
) {}

class StatusVM extends EntityVM.narrow({ type: "status" }) {}

StatusVM 复用 EntityVM 的 action 和 binder,但它的初始 Meta 会被缩窄为 { type: "status" }。这个操作只影响类型;运行时仍使用同一个 ViewModel 实现。

Meta 的这种“重绑定”此前由 .bind 承担,现已改名为 .narrow。由于 TypeScript 的类型限制,构造参数绑定和 Meta 缩窄不能由同一个 .bind API 同时可靠地表达:.bind(...args) 现在只绑定 Model 的构造参数,而 .narrow(meta) 专门处理初始 Meta。

实现 Meta 重写

重写 Meta 的属性定义

variable: helper.attribute<{
  <TMeta extends BuilderMeta, const TVarName extends string>(
    this: AR.This<TMeta>,
    variable: TVarName,
    initialValue: number,
  ): AR.WithRewriteMeta<
    { varNames: TMeta["varNames"] | TVarName },
    VariableVM
  >;
}>(() => {}),

关键点:

  • this: AR.This<TMeta> — 接收当前 meta 类型
  • AR.WithRewriteMeta<NewMeta, VMI> — 声明此属性同时打开嵌套 VM 重写 meta;VMI 为子 VM 的实例类型
  • NewMeta — 新 meta 类型,通过 TMeta["varNames"] | TVarName 追加变量名

后续属性使用 Meta

后续的属性通过 TMeta 参数获得当前的 meta 类型:

getVariable: helper.attribute<{
  <TMeta extends BuilderMeta>(
    this: AR.This<TMeta>,
    name: TMeta["varNames"], // 限制为已定义的变量名
  ): AR.Done;
}>(() => {}),
:getVariable("chakra");  // ✓ OK —— "chakra" 在 varNames 中
:getVariable("typo");    // ✗ TypeScript Error —— 未定义

AR 类型完整参考

namespace AR {
  type This<TMeta> = { "~meta": TMeta };
  type EnableIf<Cond, T> = Cond extends true ? T : never;
  type Done = { namedDefinition: { "~meta": void } };

  type With<
    VMI extends IViewModelInstance,
    TMeta = VMI["~viewModel"]["~namedDefinition"]["~meta"],
  > = {
    namedDefinition: BlockDefinitionRewriteMeta<
      VMI["~viewModel"]["~namedDefinition"],
      TMeta
    >;
  };

  type DoneRewriteMeta<NewMeta> = {
    namedDefinition: { "~meta": void };
    rewriteMeta: NewMeta;
  };

  type WithRewriteMeta<
    NewMeta,
    VMI extends IViewModelInstance,
    TMeta = VMI["~viewModel"]["~namedDefinition"]["~meta"],
  > = {
    namedDefinition: BlockDefinitionRewriteMeta<
      VMI["~viewModel"]["~namedDefinition"],
      TMeta
    >;
    rewriteMeta: NewMeta;
  };
}
返回类型含义
AR.Done无嵌套块,不修改 meta
AR.With<VM>有嵌套块,委托给子 VM
AR.DoneRewriteMeta<New>无嵌套块,但重写 meta
AR.WithRewriteMeta<New, VM>有嵌套块 + 重写 meta

完整示例:变量系统

以下展示 variablegetVariable 如何通过 Meta 重写协作:

const EntityVM = defineViewModel(
  EntityBuilder,
  (helper) => ({
    variable: helper.attribute<{
      <TMeta extends EntityMeta, const TVarName extends string>(
        this: AR.This<TMeta>,
        variable: TVarName,
        initialValue: number,
      ): AR.WithRewriteMeta<
        { varNames: TMeta["varNames"] | TVarName },
        VariableVM
      >;
    }>(() => {}),

    getVariable: helper.simpleAttribute<{
      <TMeta extends EntityMeta>(
        this: AR.This<TMeta>,
        name: TMeta["varNames"],
      ): AR.Done;
    }>()(function(this: EntityBuilder, name: string) {
      return this.variables.get(name);
    }),
  }),
  { varNames: never } as EntityMeta,
);

类型检查流程

define status {
  id 114072 as ChakraDesiderataStatus;
  variable chakra, 0;              // meta: { varNames: "chakra" }
  on useSkill {
    // 当前 meta: { varNames: "chakra" }
    const val = :getVariable("chakra");  // ✓
    // :getVariable("wrong") 将被 TypeScript 报错
  }
}

Meta 重写的适用范围

Meta 类型只在编译时存在——它不产生任何运行时代码。transpiler 的 Volar 变换管道使用 Meta 信息生成类型别名,TypeScript 用这些类型验证 GTS 定义的合法性。

常见的 Meta 追踪场景:

  • 变量名variable 追加 varNames
  • 使用次数标识usage perRound, 1 { name foo } 追加 usage 名
  • 实体类型特定约束 — 根据实体类型限制可用属性

On this page