Bondage Studio Bondage Studio
☰ 文档

平台集成

通过 window.__bmmHost 把 BMM 嵌入宿主(浏览器外壳、Electron、反代客户端)—— 拦截存储与网络、锁定设置、驱动 UI。

BMM 运行在宿主页面内部 —— 可能是普通浏览器标签页、第三方 Electron 客户端,或反代 客户端(如 studio-bondage-club,它把 BMM 作为注入的用户脚本之一)。平台桥接让这样 的宿主能够驱动 BMM,并拦截它所持久化和拉取的内容。

如果你只想从模组或插件中读取和控制 BMM,应使用插件 API,无需宿主对象。

工作原理

宿主通过在 BMM 包运行之前document-start)于 window.__bmmHost 放置一个 BmmHost 对象来表明自身。BMM 在启动时将其捕获进模块闭包并删除该全局变量,使后续 (不受信任的)模组脚本无法读取或冒充它。

window.__bmmHost ──(启动时捕获)──► PlatformBridge
   宿主 → BMM:存储、fetch、设置、UI/生命周期 标志
   BMM → 宿主:host.onReady(api) + host.onEvent(event)

没有宿主时,BMM 的行为与纯浏览器安装完全一致,且 window.bmm.api 仍会发布给插件。

宿主对象

每个字段都是可选的 —— 只提供平台需要的部分。

// 由宿主在 document-start、BMM 脚本标签之前注入。
window.__bmmHost = {
  version: 1,
  platform: {
    id: "studio-bondage-club",
    name: "Studio Bondage Club",
    version: "1.4.2",
    capabilities: ["storage", "fetch", "reload"],
  },

  // --- UI / 生命周期集成 ---
  ui: {
    hideLauncher: true,       // 宿主自带入口
    autoOpen: "mod-manager",  // 启动时直接打开某个页面
    suppressReload: true,     // 由宿主接管刷新(见 reloadRequested)
  },

  // --- 锁定设置(在 BMM UI 中只读)---
  settings: { modCacheEnabled: false },

  // --- 接管存储(替代 localStorage 作为 BMM 的存储后端)---
  storage: {
    getItem: (k) => myKvGet(k),
    setItem: (k, v) => myKvSet(k, v),
    removeItem: (k) => myKvDelete(k),
    clear: () => myKvClear(),
  },

  // --- 拦截 BMM 的数据请求 ---
  // 作用于 registry manifest、eval 模组源和缓存校验。
  // 注意:<script src> 元素加载仍由浏览器原样发起。
  fetch: (url, init) => myProxiedFetch(url, init),

  // --- BMM → 宿主 ---
  onReady: (api) => { myHost.bmm = api; },       // 就绪的公开 API
  onEvent: (event) => myHost.dispatch(event),    // { type, payload }
};

能力参考

字段方向作用
platform宿主 → BMM通过 api.platform、日志和 UI 体现的身份。
ui.hideLauncher宿主 → BMM完全隐藏悬浮启动器。
ui.autoOpen宿主 → BMMBMM 挂载后打开指定页面。
ui.suppressReload宿主 → BMMBMM 改为发出 reloadRequested 事件,而非 location.reload()
settings宿主 → BMM锁定设置;被锁定的键覆盖存储值并拒绝 UI 写入。
storage宿主 → BMM替代 localStorage 作为 BMM 的存储后端。
fetch宿主 → BMM覆盖 registry / eval 源 / 缓存校验的请求。
onReady(api)BMM → 宿主就绪时接收公开 API。
onEvent(event)BMM → 宿主接收生命周期/状态事件。

交给 onReady 的 API 与发给 onEvent 的事件,与插件使用的是完全相同的接口面 —— 完整的方法与事件参考见插件 API

获取 Mod SDK

BMM 拥有并初始化 BC Mod SDK, 并将其发布为 window.bcModSdk。若宿主想注册自己的 hook 或 patch,应通过交给 onReady 的 API 获取 SDK,而不要直接读取该全局变量 —— onReady 在 BMM 解决了与 BC 自带 SDK 的竞争之后才触发,并保证该实例是权威实例:

window.__bmmHost = {
  platform: { id: "studio-bondage-club", name: "Studio Bondage Club" },
  onReady(api) {
    if (api.sdk.isHijacked()) {
      console.warn("BC 自带的 SDK 赢得了竞争;诊断能力受限");
    }

    // 用同一个 SDK 实例注册宿主自己的 mod。
    const mod = api.sdk.registerMod({
      name: "StudioHost",
      fullName: "Studio Bondage Club host",
      version: "1.4.2",
    });
    mod?.hookFunction("ServerSend", 1, (args, next) => next(args));

    // ……或获取原始 SDK 全局以获得完整访问:
    const sdk = api.sdk.get(); // ModSDKGlobalAPI | null
  },
};

为什么走 api.sdk 而非 window.bcModSdk

  • 时机。 宿主在 document-start 注入 __bmmHost,此时 BMM 尚未创建 SDK。等到 onReady 运行时 window.bcModSdk 已存在;api.sdk.get() 让你免去轮询。
  • 权威实例。 BMM 可能用自己的 SDK 替换已存在的实例。api.sdk 始终返回 BMM (以及所有 mod)实际使用的那个实例。
  • 劫持感知。 api.sdk.isHijacked() 会告诉你 BC 自带的 SDK 先加载且无法被替换的 情况。

SDK 接口(registerModhookFunctionpatchFunction 等)由上游文档说明,并在 插件 API 中作了概述。

模式

就绪后驱动 BMM。 onReady 在 API 一存在时就交给你,无需轮询:

window.__bmmHost = {
  platform: { id: "electron-foo", name: "Foo Client" },
  onReady(api) {
    api.events.on("modsChanged", (configs) => syncToDisk(configs));
    if (api.mods.list().length === 0) api.ui.open("mod-manager");
  },
};

接管刷新。 当嵌入视图无法自行 location.reload() 时,设置 ui.suppressReload 并处理事件:

window.__bmmHost = {
  ui: { suppressReload: true },
  onEvent(event) {
    if (event.type === "reloadRequested") myWebview.reload();
  },
};

按账号隔离存储。 提供一个以当前登录角色为键的 storage 后端,使每个账号各自 保留自己的模组集合:

window.__bmmHost = {
  storage: {
    getItem: (k) => accountStore.get(currentAccount, k),
    setItem: (k, v) => accountStore.set(currentAccount, k, v),
    removeItem: (k) => accountStore.del(currentAccount, k),
  },
};

边界

  • 不是安全边界。 捕获并删除 window.__bmmHost 只是尽力而为的加固,并非隔离。 同页的模组脚本共享页面;需要真正隔离的宿主必须在自身层面强制实施(例如反代的 逐帧能力令牌)。
  • <script src> 加载不经代理。 fetch 覆盖仅作用于数据请求。若要完全控制模组 脚本的加载,请在网络层拦截 —— 反代已对 <script src> 这样做。
  • 启动器停靠位置(一项装饰性 UI 偏好)仍直接使用 localStorage,不经宿主 storage