From 5199bce57d4979d8acd243b2bc708d392b4b8a62 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Jul 2026 11:10:08 +0000 Subject: [PATCH] =?UTF-8?q?fix(ci):=20=E4=BF=AE=E5=A4=8D=20eslint-plugin?= =?UTF-8?q?=20=E6=9E=84=E5=BB=BA=E4=B8=8E=E6=AE=8B=E7=BC=BA=20land=20?= =?UTF-8?q?=E5=AF=BC=E8=87=B4=E7=9A=84=20ootb=20=E7=BA=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BLOCKER 1 — DRY/.gitignore:configs/ 误伤 **/configs/**, @sfmc-bds/eslint-plugin 的 src/configs/recommended.ts 未入库 → TS2307。 改为 /configs/,补回规则注册表与 flat config 工厂。 BLOCKER 2 — DIP/LSP:主仓 packages/land 依赖未安装的 feature-economy, check-modules 与 loadManifestV2 启动失败。按「业务模块外置、开箱仅 afk」 移除残缺 land,恢复 catalog/lock;land/economy 经 fetch-module 安装。 Co-authored-by: Shiroha --- .gitignore | 3 +- modules/catalog.json | 16 -- modules/module-lock.json | 6 +- .../packages/land/configs-default/land.json | 40 --- modules/packages/land/package.json | 15 -- modules/packages/land/sapi/manifest.json | 255 ------------------ modules/packages/land/sapi/src/index.ts | 117 -------- modules/packages/land/sapi/src/land-audit.ts | 40 --- modules/packages/land/sapi/src/land-gui.ts | 234 ---------------- .../packages/land/sapi/src/land-transfer.ts | 138 ---------- .../packages/land/sapi/src/land-validate.ts | 148 ---------- modules/packages/land/sapi/src/types.ts | 212 --------------- modules/packages/land/sapi/tsconfig.json | 8 - .../dist/configs/recommended.d.ts | 28 ++ .../dist/configs/recommended.js | 39 +++ .../sdk/@sfmc-eslint-plugin/dist/index.d.ts | 4 + modules/sdk/@sfmc-eslint-plugin/dist/index.js | 4 + .../src/configs/recommended.ts | 48 ++++ modules/sdk/@sfmc-eslint-plugin/src/index.ts | 8 +- package-lock.json | 10 +- 20 files changed, 134 insertions(+), 1239 deletions(-) delete mode 100644 modules/packages/land/configs-default/land.json delete mode 100644 modules/packages/land/package.json delete mode 100644 modules/packages/land/sapi/manifest.json delete mode 100644 modules/packages/land/sapi/src/index.ts delete mode 100644 modules/packages/land/sapi/src/land-audit.ts delete mode 100644 modules/packages/land/sapi/src/land-gui.ts delete mode 100644 modules/packages/land/sapi/src/land-transfer.ts delete mode 100644 modules/packages/land/sapi/src/land-validate.ts delete mode 100644 modules/packages/land/sapi/src/types.ts delete mode 100644 modules/packages/land/sapi/tsconfig.json create mode 100644 modules/sdk/@sfmc-eslint-plugin/dist/configs/recommended.d.ts create mode 100644 modules/sdk/@sfmc-eslint-plugin/dist/configs/recommended.js create mode 100644 modules/sdk/@sfmc-eslint-plugin/src/configs/recommended.ts diff --git a/.gitignore b/.gitignore index f13c8500..7bc1b1af 100644 --- a/.gitignore +++ b/.gitignore @@ -37,7 +37,8 @@ memory/ data/ .AddCompLib/* *.zip -configs/ +# 仅忽略仓根运行时 configs/(勿写成 configs/,否则会误伤 **/configs/**,如 eslint-plugin) +/configs/ .workbuddy/ BDS diff --git a/modules/catalog.json b/modules/catalog.json index 9d547da6..1d72e8a8 100644 --- a/modules/catalog.json +++ b/modules/catalog.json @@ -15,22 +15,6 @@ "kind": "sapi", "path": "modules/packages/afk/sapi/src/index.ts" } - }, - { - "id": "feature-land", - "configKey": "land", - "name": "领地系统", - "type": "feature", - "description": "", - "enabledByDefault": false, - "canDisable": true, - "requires": [ - "feature-economy" - ], - "entry": { - "kind": "sapi", - "path": "modules/packages/land/sapi/src/index.ts" - } } ] } diff --git a/modules/module-lock.json b/modules/module-lock.json index 6e37272e..c427407c 100644 --- a/modules/module-lock.json +++ b/modules/module-lock.json @@ -19,11 +19,7 @@ }, "feature-afk": { "enabled": false, - "updatedAt": 1784797283548 - }, - "feature-land": { - "enabled": false, - "updatedAt": 1784797269932 + "updatedAt": 1784701928968 } } } diff --git a/modules/packages/land/configs-default/land.json b/modules/packages/land/configs-default/land.json deleted file mode 100644 index 583732d5..00000000 --- a/modules/packages/land/configs-default/land.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "_comment": "领地系统配置。SAPI 通过 /api/sfmc/settings/land:* 直接读取此文件。", - - "land:config": { - "priceFormula": "{square}*8+{height}*20", - "maxLandsPerPlayer": 5, - "minSquare": 4, - "maxSquare": 50000, - "discount": 1, - "refundRate": 0.7 - }, - - "land:plaza": { - "name": "公共广场", - "welcome": "欢迎来到服务器!这里是公共土地,所有人都可以建造。", - "dimid": 0, - "range": 32 - }, - - "land:tax": { - "enabled": false, - "defaultRate": 50, - "periodDays": 7, - "freezeOnInsufficient": true, - "fallbackPurchasePrice": 100 - }, - - "land:permissions": { - "allow_place": false, - "allow_destroy": false, - "attack_entity": false, - "open_container": false, - "use_door": false, - "use_button": false, - "use_redstone": false, - "interact_entity": false, - "pickup_item": false - } -} - diff --git a/modules/packages/land/package.json b/modules/packages/land/package.json deleted file mode 100644 index 4451cd5c..00000000 --- a/modules/packages/land/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "@sfmc-bds/module-land", - "version": "0.1.0", - "private": true, - "type": "module", - "description": "SAPI module: 领地系统(含 GUI)。SAPI 侧入口,sapi/src/index.ts。", - "main": "sapi/src/index.ts", - "scripts": { - "typecheck": "tsc --noEmit -p sapi/tsconfig.json" - }, - "dependencies": { - "@sfmc-bds/sdk": "*", - "@sfmc-bds/module-economy": "*" - } -} diff --git a/modules/packages/land/sapi/manifest.json b/modules/packages/land/sapi/manifest.json deleted file mode 100644 index f2fa4f9b..00000000 --- a/modules/packages/land/sapi/manifest.json +++ /dev/null @@ -1,255 +0,0 @@ -{ - "$schema": "../../../node_modules/@sfmc-bds/sdk/schemas/sapi-manifest.v2.schema.json", - "schemaVersion": 2, - "id": "feature-land", - "name": "领地系统", - "type": "feature", - "configKey": "land", - "requires": ["feature-economy"], - "permissions": [ - "db:read:lands", - "db:write:lands", - "db:read:land_members", - "db:write:land_members", - "db:read:land_audit_logs", - "db:write:land_audit_logs", - "db:read:land_operations", - "db:write:land_operations", - "config:read:land", - "config:write:land", - "service:economy.account.debit", - "service:economy.account.credit", - "service:economy.account.get" - ], - "services": { - "provides": [ - { - "name": "land.byOwner", - "input": { - "type": "object", - "properties": { - "ownerId": { - "type": "string" - } - }, - "required": ["ownerId"] - }, - "output": { - "type": "array" - } - }, - { - "name": "land.byId", - "input": { - "type": "object", - "properties": { - "landId": { - "type": "string" - } - }, - "required": ["landId"] - }, - "output": { - "type": "object" - } - }, - { - "name": "land.byPos", - "input": { - "type": "object", - "properties": { - "dimension": { - "type": "integer" - }, - "x": { - "type": "integer" - }, - "y": { - "type": "integer" - }, - "z": { - "type": "integer" - } - }, - "required": ["dimension", "x", "y", "z"] - }, - "output": { - "type": "object" - } - }, - { - "name": "land.listByOwner", - "input": { - "type": "object", - "properties": { - "ownerId": { - "type": "string" - } - }, - "required": ["ownerId"] - }, - "output": { - "type": "array" - } - }, - { - "name": "land.listMembers", - "input": { - "type": "object", - "properties": { - "landId": { - "type": "string" - } - }, - "required": ["landId"] - }, - "output": { - "type": "array" - } - }, - { - "name": "land.listInvites", - "input": { - "type": "object", - "properties": { - "playerId": { - "type": "string" - } - }, - "required": ["playerId"] - }, - "output": { - "type": "array" - } - }, - { - "name": "land.getPlaza", - "input": { - "type": "object" - }, - "output": { - "type": "object" - } - }, - { - "name": "land.plazaSettings", - "input": { - "type": "object" - }, - "output": { - "type": "object" - } - }, - { - "name": "land.getPlayerRole", - "input": { - "type": "object", - "properties": { - "landId": { - "type": "string" - }, - "playerId": { - "type": "string" - } - }, - "required": ["landId", "playerId"] - }, - "output": { - "type": "string" - } - }, - { - "name": "land.validateBox", - "input": { - "type": "object", - "properties": { - "dimension": { - "type": "integer" - }, - "minX": { - "type": "integer" - }, - "minY": { - "type": "integer" - }, - "minZ": { - "type": "integer" - }, - "maxX": { - "type": "integer" - }, - "maxY": { - "type": "integer" - }, - "maxZ": { - "type": "integer" - } - } - }, - "output": { - "type": "object" - } - }, - { - "name": "land.taxDue", - "input": { - "type": "object", - "properties": { - "landId": { - "type": "string" - } - }, - "required": ["landId"] - }, - "output": { - "type": "object" - } - }, - { - "name": "land.auditLog", - "input": { - "type": "object", - "properties": { - "landId": { - "type": "string" - } - }, - "required": ["landId"] - }, - "output": { - "type": "array" - } - } - ], - "requires": [ - { - "name": "economy.account.debit", - "input": { - "type": "object" - }, - "output": { - "type": "object" - } - }, - { - "name": "economy.account.credit", - "input": { - "type": "object" - }, - "output": { - "type": "object" - } - }, - { - "name": "economy.account.get", - "input": { - "type": "object" - }, - "output": { - "type": "object" - } - } - ] - }, - "notes": "v2 标准例子。领地数据 + GUI 合一(原 feature-land-gui 已并入)。跨模块扣款走 economy.account.inTx(tx).debit/credit(来自 @sfmc-bds/module-economy/client);购地余额校验走 SDK Money / economy.account.get。" -} diff --git a/modules/packages/land/sapi/src/index.ts b/modules/packages/land/sapi/src/index.ts deleted file mode 100644 index 6f356ec8..00000000 --- a/modules/packages/land/sapi/src/index.ts +++ /dev/null @@ -1,117 +0,0 @@ -/** - * @sfmc-bds/module-land — v2 入口(含领地 GUI) - * - * ModuleRegistry + db.defineTable。跨模块扣款走 @sfmc-bds/module-economy/client。 - * land.* service provides 在 manifest 声明;GUI(原 feature-land-gui)已并入本模块。 - */ - -import { db } from "@sfmc-bds/sdk/sapi/db"; -import { ModuleRegistry } from "@sfmc-bds/sdk/module-loader"; -import { Permission } from "@sfmc-bds/sdk/sapi/runtime"; -import { registerLandGuiCommands } from "./land-gui.js"; - -const MODULE_ID = "feature-land"; - -ModuleRegistry.register({ - id: MODULE_ID, - afterWorldLoad: false, - lifecycle: { - registerPermissions() { - Permission.register("land.use", Permission.Any); - Permission.register("land.admin", Permission.OP); - Permission.register("land.gui.use", Permission.Any); - Permission.register("land.gui.admin", Permission.OP); - }, - - async init() { - await db.defineTable( - "lands", - { - id: { type: "TEXT", primary: true }, - owner_player_id: { type: "TEXT", notNull: true, index: true }, - owner_name_snapshot: { type: "TEXT", default: "" }, - dimension: { type: "INTEGER", notNull: true }, - min_x: { type: "INTEGER", notNull: true }, - min_y: { type: "INTEGER", notNull: true }, - min_z: { type: "INTEGER", notNull: true }, - max_x: { type: "INTEGER", notNull: true }, - max_y: { type: "INTEGER", notNull: true }, - max_z: { type: "INTEGER", notNull: true }, - name: { type: "TEXT", default: "" }, - status: { type: "TEXT", default: "active", index: true }, - created_at: { type: "INTEGER", notNull: true }, - updated_at: { type: "INTEGER", notNull: true }, - expires_at: { type: "INTEGER" }, - protection_profile: { type: "TEXT", default: "{}" }, - version: { type: "INTEGER", default: 1 }, - purchase_price: { type: "INTEGER", default: 0 }, - refund_rate: { type: "REAL", default: 0.7 }, - tax_rate: { type: "INTEGER", default: 0 }, - tax_due_at: { type: "INTEGER" }, - tax_frozen: { type: "INTEGER", default: 0 }, - }, - { softDelete: true } - ); - - await db.defineTable( - "land_members", - { - id: { type: "TEXT", primary: true }, - land_id: { type: "TEXT", notNull: true, index: true }, - player_id: { type: "TEXT", notNull: true, index: true }, - player_name_snapshot: { type: "TEXT", default: "" }, - role: { type: "TEXT", default: "admin" }, - created_at: { type: "INTEGER", notNull: true }, - expires_at: { type: "INTEGER" }, - }, - { softDelete: true } - ); - - await db.defineTable( - "land_audit_logs", - { - id: { type: "INTEGER", primary: true }, - land_id: { type: "TEXT", notNull: true, index: true }, - actor_id: { type: "TEXT", notNull: true }, - action: { type: "TEXT", notNull: true, index: true }, - payload: { type: "TEXT", default: "{}" }, - created_at: { type: "INTEGER", notNull: true }, - }, - { softDelete: false } - ); - - await db.defineTable( - "land_operations", - { - request_id: { type: "TEXT", primary: true }, - operation_type: { type: "TEXT", notNull: true }, - actor_id: { type: "TEXT", notNull: true }, - land_id: { type: "TEXT" }, - status: { type: "TEXT", notNull: true }, - response_json: { type: "TEXT", notNull: true, default: "{}" }, - created_at: { type: "INTEGER", notNull: true }, - }, - { softDelete: false } - ); - - registerLandGuiCommands(); - }, - - cleanup() {}, - }, -}); - -export { transferLand } from "./land-transfer.js"; -export { validateLandBox, findLandsByOwner } from "./land-validate.js"; -export { queryAuditLog } from "./land-audit.js"; -export type * from "./types.js"; -export { - LandGUI, - LandApi, - getSession, - initSession, - setPos, - clearSession, - registerLandGuiCommands, -} from "./land-gui.js"; -export type { InviteRow, LandGuiLandRow, LandGuiMemberRow } from "./land-gui.js"; diff --git a/modules/packages/land/sapi/src/land-audit.ts b/modules/packages/land/sapi/src/land-audit.ts deleted file mode 100644 index 89bbf2bd..00000000 --- a/modules/packages/land/sapi/src/land-audit.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * land-audit.ts — 领地审计日志查询 - * - * 不走事务(只读),走 db.query。 - */ - -import { db } from "@sfmc-bds/sdk/sapi/db"; -import type { WhereExpr } from "@sfmc-bds/sdk/sapi/db"; - -export interface AuditRow extends Record { - id: number; - land_id: string; - actor_id: string; - action: string; - payload: Record; - created_at: number; -} - -export async function queryAuditLog(opts: { - landId?: string; - actorId?: string; - action?: string; - limit?: number; -} = {}): Promise { - const conditions: WhereExpr[] = []; - if (opts.landId) conditions.push({ eq: ["land_id", opts.landId] }); - if (opts.actorId) conditions.push({ eq: ["actor_id", opts.actorId] }); - if (opts.action) conditions.push({ eq: ["action", opts.action] }); - const where: WhereExpr | undefined = - conditions.length === 0 - ? undefined - : conditions.length === 1 - ? conditions[0]! - : { and: conditions }; - return db.query("land_audit_logs", { - ...(where ? { where } : {}), - orderBy: { field: "created_at", dir: "desc" }, - limit: opts.limit ?? 50, - }); -} \ No newline at end of file diff --git a/modules/packages/land/sapi/src/land-gui.ts b/modules/packages/land/sapi/src/land-gui.ts deleted file mode 100644 index 53985875..00000000 --- a/modules/packages/land/sapi/src/land-gui.ts +++ /dev/null @@ -1,234 +0,0 @@ -/** - * land-gui.ts — 领地 GUI(已并入 feature-land) - * - * 原 @sfmc-bds/module-land-gui 全部迁入本文件。 - * 土地数据走 land.* service / db.query;购地前余额校验走 SDK Money。 - */ - -import { Player } from "@minecraft/server"; -import { db } from "@sfmc-bds/sdk/sapi/db"; -import { service } from "@sfmc-bds/sdk/sapi/service"; -import { config } from "@sfmc-bds/sdk/sapi/config"; -import { - debug, - Msg, - MenuNavigator, - ListFormInfo, - dimensionId, - Money, -} from "@sfmc-bds/sdk/sapi/runtime"; - -export interface LandGuiLandRow extends Record { - id: string; - owner_player_id: string; - owner_name_snapshot: string; - dimension: number; - min_x: number; - min_y: number; - min_z: number; - max_x: number; - max_y: number; - max_z: number; - name: string; - status: string; - version: number; -} - -export interface LandGuiMemberRow { - id: string; - land_id: string; - player_id: string; - player_name_snapshot: string; - role: string; -} - -export interface InviteRow { - invite_id: string; - land_id: string; - expires_at: number; -} - -/** @deprecated 兼容旧名;请优先用 LandGuiLandRow */ -export type LandRow = LandGuiLandRow; -/** @deprecated 兼容旧名;请优先用 LandGuiMemberRow */ -export type LandMemberRow = LandGuiMemberRow; - -/* ── 命令挂载(占位;真实 UI 入口由 LandGUI.showMainMenu / 主菜单调用) ── */ - -export function registerLandGuiCommands(): void { - // 实际命令注册走 @sfmc-bds/sdk/runtime 的 Command.register - debug.i("LAND-GUI", "registerLandGuiCommands: SDK 占位 OK"); -} - -/* ── Land API SDK facade ─────────────────────────────────────── */ - -/** - * 对外 facade:GUI / 主菜单从此取土地数据。 - * 同模块内亦可直接走 db / service,保持与历史 land-gui 行为一致。 - */ -export const LandApi = { - async getLandById(landId: string): Promise { - return service.get("land.byId", { landId }); - }, - - async getLandByPos(dimension: number, x: number, y: number, z: number): Promise { - return service.get("land.byPos", { dimension, x, y, z }); - }, - - async listLandsByOwner(ownerId: string): Promise { - return service.get("land.listByOwner", { ownerId }); - }, - - async listMembers(landId: string): Promise { - return service.get("land.listMembers", { landId }); - }, - - async listInvites(playerId: string): Promise { - return service.get("land.listInvites", { playerId }); - }, - - async getPlaza(): Promise { - return service.get("land.getPlaza", {}); - }, - - async plazaSettings(): Promise<{ name: string; welcome: string; dimid: number; range: number }> { - return service.get<{ name: string; welcome: string; dimid: number; range: number }>("land.plazaSettings", {}); - }, - - async getPlayerRole(landId: string, playerId: string): Promise { - return service.get("land.getPlayerRole", { landId, playerId }); - }, - - async auditLog(landId: string): Promise> { - return service.get>("land.auditLog", { - landId, - }); - }, - - /** 直接读 lands 表(单点查询,避免 service.get 序列化开销) */ - async queryLands(where: Parameters[1]): Promise { - return db.query("lands", where); - }, -}; - -/* ── 本地会话(申请土地点位) ──────────────────────────────────── */ - -interface PlayerSession { - pos1?: { x: number; y: number; z: number }; - pos2?: { x: number; y: number; z: number }; - dimensionId?: number; - updatedAt: number; -} - -const _sessions = new Map(); - -export function getSession(plid: string): PlayerSession | undefined { - return _sessions.get(plid); -} - -export function initSession(plid: string): void { - _sessions.set(plid, { updatedAt: Date.now() }); -} - -export function setPos(plid: string, idx: 1 | 2, pos: { x: number; y: number; z: number }): void { - const s = _sessions.get(plid) ?? { updatedAt: Date.now() }; - if (idx === 1) s.pos1 = pos; - else s.pos2 = pos; - s.updatedAt = Date.now(); - _sessions.set(plid, s); -} - -export function clearSession(plid: string): boolean { - return _sessions.delete(plid); -} - -/* ── LandGUI 类(简化版,展示 SDK 调用骨架) ─────────────────── */ - -type LandGuiState = { - selectedLandId?: string; - invites: InviteRow[]; - loading: boolean; -}; - -export class LandGUI { - private nav: MenuNavigator; - private player: Player; - - private constructor(player: Player) { - this.player = player; - this.nav = new MenuNavigator(player); - this.nav.state.gui = { invites: [], loading: false } satisfies LandGuiState; - } - - static showMainMenu(player: Player): void { - debug.i("GUI", `LandGUI.showMainMenu: player=${player.name}`); - const gui = new LandGUI(player); - void LandApi.listInvites(player.id).then((invites) => { - gui.state.invites = invites; - return gui.nav.start("home"); - }); - } - - private get state(): LandGuiState { - return this.nav.state.gui as LandGuiState; - } - - async buildHome(page: { - label(s: string): void; - button(s: string, fn: () => void | Promise): void; - }): Promise { - const pos = this.player.location; - const dim = dimensionId(this.player.dimension); - const landHere = await LandApi.getLandByPos(dim, Math.floor(pos.x), Math.floor(pos.y), Math.floor(pos.z)); - const owned = await LandApi.listLandsByOwner(this.player.id); - page.label( - ListFormInfo([ - landHere ? `当前土地:${landHere.name || landHere.id}` : "当前不在土地保护范围内。", - `拥有土地:${owned.length} 块`, - `待处理邀请:${this.state.invites.length} 项`, - ]) - ); - page.button("我的土地", () => void this.nav.rebuild("landList")); - page.button("申请土地", () => void this.openApplication()); - } - - private async openApplication(): Promise { - const session = getSession(this.player.id); - if (!session || !session.pos1 || !session.pos2) { - Msg.info("请先使用 !pos1 和 !pos2 命令选择土地范围。", this.player); - return; - } - const cfg = (await config.get<{ - minSquare: number; - maxSquare: number; - maxLandsPerPlayer: number; - discount: number; - }>("land.config")) ?? { - minSquare: 9, - maxSquare: 10000, - maxLandsPerPlayer: 5, - discount: 1, - }; - const valid = await service.get<{ ok: boolean; msg?: string }>("land.validateBox", { - dimension: session.dimensionId ?? 0, - minX: session.pos1.x, - minY: session.pos1.y, - minZ: session.pos1.z, - maxX: session.pos2.x, - maxY: session.pos2.y, - maxZ: session.pos2.z, - }); - if (!valid.ok) { - Msg.error(valid.msg ?? "土地验证失败。", this.player); - return; - } - const balance = await Money.load(this.player); - const sq = (session.pos2.x - session.pos1.x + 1) * (session.pos2.z - session.pos1.z + 1); - const price = Math.max(0, Math.floor((sq * 8 + 1) * cfg.discount)); - if (balance < price) { - Msg.error(`${Money.UNIT} 不足,需要 ${price}。`, this.player); - return; - } - Msg.success(`土地验证通过,价格 ${price} ${Money.UNIT},请通过 land v2 服务创建。`, this.player); - } -} diff --git a/modules/packages/land/sapi/src/land-transfer.ts b/modules/packages/land/sapi/src/land-transfer.ts deleted file mode 100644 index caccbcdd..00000000 --- a/modules/packages/land/sapi/src/land-transfer.ts +++ /dev/null @@ -1,138 +0,0 @@ -/** - * land-transfer.ts — 领地转让(走 db.tx) - * - * v2 协议:业务逻辑只描述"我想让 db-server 做什么",真实执行在 db-server 进程内。 - * 事务边界由 db.tx() 划定: - * - 更新 lands.owner_player_id / owner_name_snapshot - * - 写 land_audit_logs(action=transfer) - * - 删原 land_members(owner),新增新 owner member 行 - * - 若新 owner = 当前 player = 自己 = 报错 - * - * 注:跨模块扣款在 tx 内通过 economy.account.inTx(tx).debit/credit 走。 - */ - -import { db, type TxContext, DbError } from "@sfmc-bds/sdk/sapi/db"; -import { economy } from "@sfmc-bds/module-economy/client"; - -export interface TransferInput { - landId: string; - currentOwnerId: string; - newOwnerId: string; - newOwnerName: string; - transferPrice: number; - requestId: string; -} - -export interface TransferResult { - ok: boolean; - landId: string; - newOwnerId: string; - newBalance?: number; - error?: string; - message?: string; -} - -export async function transferLand(input: TransferInput): Promise { - if (input.currentOwnerId === input.newOwnerId) { - return { ok: false, landId: input.landId, newOwnerId: input.newOwnerId, error: "self_transfer", message: "不能转让给自己。" }; - } - - try { - return await db.tx(async (tx) => { - const snapshot = await runTransferSteps(tx, input); - return snapshot; - }); - } catch (e) { - if (e instanceof DbError) { - return { ok: false, landId: input.landId, newOwnerId: input.newOwnerId, error: e.code, message: e.message }; - } - throw e; - } -} - -async function runTransferSteps(tx: TxContext, input: TransferInput): Promise { - const now = Date.now(); - const oldMemberId = synthMemberId(input.landId, input.currentOwnerId); - const newMemberId = synthMemberId(input.landId, input.newOwnerId); - - // 1. 拿当前 land(版本号) - const cur = await tx.get<{ - id: string; - owner_player_id: string; - version: number; - status: string; - }>("lands", input.landId); - // 注意:tx.get 在 SDK 端只是 step recorder,这里拿不到真实值 — 用闭包假设 - // 真实执行发生在 db-server 内,client 端假设步骤会被原子执行。 - void cur; - - // 2. 删原 owner 成员行 - await tx.delete("land_members", oldMemberId); - - // 3. 写新 owner 成员行 - await tx.insert("land_members", { - id: newMemberId, - land_id: input.landId, - player_id: input.newOwnerId, - player_name_snapshot: input.newOwnerName, - role: "admin", - created_at: now, - }); - - // 4. 更新 land owner - await tx.update("lands", input.landId, { - owner_player_id: input.newOwnerId, - owner_name_snapshot: input.newOwnerName, - version: ((cur?.version ?? 0) as number) + 1, - updated_at: now, - }); - - // 5. 写审计 - await tx.audit("lands", input.landId, "transfer", { - from: input.currentOwnerId, - to: input.newOwnerId, - price: input.transferPrice, - requestId: input.requestId, - }); - - // 6. 写 land_operations(idempotency 兜底) - await tx.insert("land_operations", { - request_id: input.requestId, - operation_type: "transfer", - actor_id: input.currentOwnerId, - land_id: input.landId, - status: "pending", - response_json: JSON.stringify({ to: input.newOwnerId }), - created_at: now, - }); - - // 7. 跨模块:扣款 + 加款(事务内走 economy client) - if (input.transferPrice > 0) { - const eco = economy.account.inTx(tx); - await eco.debit({ - playerId: input.currentOwnerId, - amount: input.transferPrice, - reason: `land.transfer:${input.landId}`, - }); - await eco.credit({ - playerId: input.newOwnerId, - amount: input.transferPrice, - reason: `land.transfer.receive:${input.landId}`, - }); - } - - return { - ok: true, - landId: input.landId, - newOwnerId: input.newOwnerId, - }; -} - -function synthMemberId(landId: string, playerId: string): string { - // 简单 hash:landId|playerId → 16 字符 (用 djb2) - let hash = 5381; - const s = `${landId}|${playerId}`; - for (let i = 0; i < s.length; i++) hash = ((hash << 5) + hash + s.charCodeAt(i)) | 0; - const hex = (hash >>> 0).toString(16).padStart(8, "0"); - return `lm_${hex}`; -} \ No newline at end of file diff --git a/modules/packages/land/sapi/src/land-validate.ts b/modules/packages/land/sapi/src/land-validate.ts deleted file mode 100644 index 792a9c82..00000000 --- a/modules/packages/land/sapi/src/land-validate.ts +++ /dev/null @@ -1,148 +0,0 @@ -/** - * land-validate.ts — 领地合法性校验(走 db.tx) - * - * 关键:tx() 内部 recorder 不返回真实结果,所有"读"必须依靠 fn 闭包传入的外部 - * snapshot;返回 ValidationResult 也走 fn 返回值(不依赖 tx 内的查询结果)。 - * - * 用法: - * const r = await db.tx(async (tx) => { - * const cfg = configSnapshot.discount; // 从外部注入 - * // tx.query/get 仅作为 step 记录,真实查询在 platform side 完成 - * return calcByCube(cfg, info); - * }); - */ - -import type { TxContext } from "@sfmc-bds/sdk/sapi/db"; - -export interface LandCubeInput { - dimension: number; - posA: { x: number; y: number; z: number }; - posB: { x: number; y: number; z: number }; -} - -export interface CubeInfo { - length: number; - width: number; - height: number; - square: number; - volume: number; -} - -export interface ValidationResult { - ok: boolean; - msg?: string; -} - -export interface LandConfigSnapshot { - minSquare: number; - maxSquare: number; - maxLandsPerPlayer: number; - discount: number; -} - -export function normalizeBox(input: LandCubeInput): { - min: { x: number; y: number; z: number }; - max: { x: number; y: number; z: number }; -} { - return { - min: { - x: Math.min(input.posA.x, input.posB.x), - y: Math.min(input.posA.y, input.posB.y), - z: Math.min(input.posA.z, input.posB.z), - }, - max: { - x: Math.max(input.posA.x, input.posB.x), - y: Math.max(input.posA.y, input.posB.y), - z: Math.max(input.posA.z, input.posB.z), - }, - }; -} - -export function cubeInfo(input: LandCubeInput): CubeInfo { - const n = normalizeBox(input); - const w = n.max.x - n.min.x + 1; - const h = n.max.y - n.min.y + 1; - const l = n.max.z - n.min.z + 1; - return { length: l, width: w, height: h, square: w * l, volume: w * h * l }; -} - -/** - * 校验重叠 + 上限;在事务内跑(读 lands + 计数),返回结构化结果。 - * - * 调用方需自行准备 ownerLands(避免 tx 内查询结果不可用): - * const ownerLands = await db.query("lands", { where: { eq: ["owner_player_id", ownerId] } }); - * const r = await db.tx(async (tx) => validateLandBox(tx, { cfg, ownerLands, candidate })); - */ -export function validateLandBox( - tx: TxContext, - args: { - cfg: LandConfigSnapshot; - ownerLands: Array<{ id: string; dimension: number; min_x: number; min_y: number; min_z: number; max_x: number; max_y: number; max_z: number }>; - candidate: LandCubeInput; - activeLands?: number; - } -): ValidationResult { - const info = cubeInfo(args.candidate); - if (info.square < args.cfg.minSquare) { - return { ok: false, msg: `土地面积过小!最小面积为 ${args.cfg.minSquare} 格。` }; - } - if (info.square > args.cfg.maxSquare) { - return { ok: false, msg: `土地面积过大!最大面积为 ${args.cfg.maxSquare} 格。` }; - } - if (args.ownerLands.length >= args.cfg.maxLandsPerPlayer) { - return { ok: false, msg: `已达持有土地上限(${args.cfg.maxLandsPerPlayer} 块)!` }; - } - const norm = normalizeBox(args.candidate); - for (const land of args.ownerLands) { - if (land.dimension !== args.candidate.dimension) continue; - if (cubesOverlap(norm, land)) { - return { ok: false, msg: "该区域与其他土地重叠,请重新选择土地范围。" }; - } - } - // 主动 step 记录(供审计 + 后续 db-server 端校验二次执行) - void tx; - void args.activeLands; - return { ok: true }; -} - -function cubesOverlap( - a: { min: { x: number; y: number; z: number }; max: { x: number; y: number; z: number } }, - b: { min_x: number; min_y: number; min_z: number; max_x: number; max_y: number; max_z: number } -): boolean { - return ( - a.min.x <= b.max_x && - a.max.x >= b.min_x && - a.min.y <= b.max_y && - a.max.y >= b.min_y && - a.min.z <= b.max_z && - a.max.z >= b.min_z - ); -} - -/** 计算本地预览价格(非权威,服务端会重算) */ -export function calculateLocalPrice(cfg: LandConfigSnapshot, info: CubeInfo): number { - return Math.max(0, Math.floor((info.square * 8 + info.height * 20) * cfg.discount)); -} - -export interface LandOwnerRow { - id: string; - dimension: number; - min_x: number; - min_y: number; - min_z: number; - max_x: number; - max_y: number; - max_z: number; - status: string; -} - -export function findLandsByOwner( - rows: LandOwnerRow[], - ownerId: string -): LandOwnerRow[] { - return rows.filter((r) => r.status === "active" && (r as unknown as { owner_player_id?: string }).owner_player_id === ownerId); -} - -export function getDimensionName(dimid: number): string { - return ["主世界", "地狱", "末地"][dimid] ?? "未知"; -} \ No newline at end of file diff --git a/modules/packages/land/sapi/src/types.ts b/modules/packages/land/sapi/src/types.ts deleted file mode 100644 index d2c3582e..00000000 --- a/modules/packages/land/sapi/src/types.ts +++ /dev/null @@ -1,212 +0,0 @@ -/** - * types.ts — 领地数据模型(本模块权威) - * - * - snake_case 行类型对应数据库列 - * - camelCase 视图类型对应 API / 客户端 - */ - -// ── DB 行类型 ── - -export interface LandRow { - id: string; - owner_player_id: string; - owner_name_snapshot: string; - dimension: number; - min_x: number; - min_y: number; - min_z: number; - max_x: number; - max_y: number; - max_z: number; - name: string; - status: string; - protection_profile: string; - created_at: number; - updated_at: number; - version: number; - purchase_price: number; - refund_rate: number; - tax_rate?: number; - tax_due_at?: number; - tax_frozen?: number; -} - -export interface LandMemberRow { - land_id: string; - player_id: string; - player_name_snapshot: string; - role: string; - status: string; - expires_at: number | null; - created_at: number; - joined_at?: number; - version?: number; -} - -export interface LandInviteRow { - id: string; - land_id: string; - inviter_id: string; - invitee_id: string; - role: string; - status: string; - expires_at: number; - created_at: number; - invitee_name_snapshot?: string; -} - -export interface LandAuditLog { - land_id: string; - actor_id: string; - action: string; - payload: string; - created_at: number; -} - -// ── API 视图类型 ── - -export type LandRole = - | "owner" - | "admin" - | "member" - | "builder" - | "container" - | "visitor" - | "redstone" - | "entity"; - -export interface LandPos { - x: number; - y: number; - z: number; -} - -export interface LandPermissions { - allow_place: boolean; - allow_destroy: boolean; - attack_entity: boolean; - open_container: boolean; - use_door?: boolean; - use_button?: boolean; - use_redstone?: boolean; - interact_entity?: boolean; - pickup_item?: boolean; -} - -export interface LandMember { - player_id: string; - player_name_snapshot?: string; - role: LandRole; - status?: string; - expires_at?: number | null; - joined_at?: number; -} - -export interface LandData { - id: string; - ownerplid: string; - ownerName: string; - managers: string[]; - members?: LandMember[]; - dimid: number; - posA: LandPos; - posB: LandPos; - permissions: LandPermissions; - nickname: string; - createdAt: number; - status?: string; - version?: number; - purchasePrice?: number; - refundRate?: number; -} - -// ── 配置 ── - -export interface LandConfig { - priceFormula: string; - maxLandsPerPlayer: number; - minSquare: number; - maxSquare: number; - discount: number; - refundRate: number; -} - -export interface LandTaxConfig { - enabled: boolean; - defaultRate: number; - periodDays: number; - freezeOnInsufficient: boolean; - fallbackPurchasePrice: number; -} - -// ── 请求 / 响应 ── - -export interface CreateLandRequest { - ownerId: string; - ownerName: string; - dimid: number; - posA: LandPos; - posB: LandPos; - requestId?: string; -} - -export interface LandValidation { - ok: boolean; - error?: string; - price?: number; - status?: number; - square?: number; - volume?: number; - refundRate?: number; -} - -export type LandErrorCode = - | "not_found" - | "forbidden" - | "already_deleted" - | "invalid_request" - | "invalid_target" - | "invalid_role" - | "overlap" - | "land_limit" - | "insufficient_funds" - | "database_unavailable" - | "version_conflict" - | "transaction_failed" - | "request_id_conflict"; - -export interface LandApiResult { - ok: boolean; - data?: T; - error?: LandErrorCode | string; - message?: string; - status?: number; - transactionId?: string; -} - -export interface DeleteLandResult extends LandApiResult { - refund?: number; - balance?: number; - balanceBefore?: number; - balanceAfter?: number; - balanceVersion?: number; -} - -export interface TransferLandResult extends LandApiResult { - land?: LandData; -} - -export interface LandMemberInviteResult { - ok: boolean; - inviteId?: string; - expiresAt?: number; - error?: LandErrorCode | string; - message?: string; -} - -export interface LandMemberResult { - ok: boolean; - land?: LandData | null; - error?: LandErrorCode | string; - message?: string; -} diff --git a/modules/packages/land/sapi/tsconfig.json b/modules/packages/land/sapi/tsconfig.json deleted file mode 100644 index 4b69b707..00000000 --- a/modules/packages/land/sapi/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "noEmit": true, - "rootDir": "./src" - }, - "include": ["src/**/*"] -} diff --git a/modules/sdk/@sfmc-eslint-plugin/dist/configs/recommended.d.ts b/modules/sdk/@sfmc-eslint-plugin/dist/configs/recommended.d.ts new file mode 100644 index 00000000..a7b1e557 --- /dev/null +++ b/modules/sdk/@sfmc-eslint-plugin/dist/configs/recommended.d.ts @@ -0,0 +1,28 @@ +import type { ESLint, Linter } from "eslint"; +/** + * 规则实现注册表(权威来源)。 + * 新增规则只需:实现 rule → 挂到此处 → 写入 recommended/all 严重级别。 + */ +export declare const rules: { + "no-player-send-message": import("@typescript-eslint/utils/ts-eslint").RuleModule<"useMsg", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & { + name: string; + }; + "no-sfmc-sdk-alias": import("@typescript-eslint/utils/ts-eslint").RuleModule<"useOfficial", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & { + name: string; + }; + "no-sdk-deep-import": import("@typescript-eslint/utils/ts-eslint").RuleModule<"usePublic", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & { + name: string; + }; + "require-module-registry": import("@typescript-eslint/utils/ts-eslint").RuleModule<"missing", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & { + name: string; + }; +}; +/** recommended:日常模块/SDK 约定 */ +export declare const recommendedRules: Linter.RulesRecord; +/** all:在 recommended 上将 no-player-send-message 升为 error */ +export declare const allRules: Linter.RulesRecord; +/** + * 生成 flat config 片段。 + * 由调用方注入 plugin 实例,避免 configs ↔ index 循环依赖。 + */ +export declare function createFlatConfig(plugin: ESLint.Plugin, ruleSeverities: Linter.RulesRecord, name?: string): Linter.Config; diff --git a/modules/sdk/@sfmc-eslint-plugin/dist/configs/recommended.js b/modules/sdk/@sfmc-eslint-plugin/dist/configs/recommended.js new file mode 100644 index 00000000..453686cb --- /dev/null +++ b/modules/sdk/@sfmc-eslint-plugin/dist/configs/recommended.js @@ -0,0 +1,39 @@ +import { noPlayerSendMessage } from "../rules/no-player-send-message.js"; +import { noSdkDeepImport } from "../rules/no-sdk-deep-import.js"; +import { noSfmcSdkAlias } from "../rules/no-sfmc-sdk-alias.js"; +import { requireModuleRegistry } from "../rules/require-module-registry.js"; +/** + * 规则实现注册表(权威来源)。 + * 新增规则只需:实现 rule → 挂到此处 → 写入 recommended/all 严重级别。 + */ +export const rules = { + "no-player-send-message": noPlayerSendMessage, + "no-sfmc-sdk-alias": noSfmcSdkAlias, + "no-sdk-deep-import": noSdkDeepImport, + "require-module-registry": requireModuleRegistry, +}; +/** recommended:日常模块/SDK 约定 */ +export const recommendedRules = { + "@sfmc-bds/no-player-send-message": "warn", + "@sfmc-bds/no-sfmc-sdk-alias": "error", + "@sfmc-bds/no-sdk-deep-import": "error", + "@sfmc-bds/require-module-registry": "warn", +}; +/** all:在 recommended 上将 no-player-send-message 升为 error */ +export const allRules = { + ...recommendedRules, + "@sfmc-bds/no-player-send-message": "error", +}; +/** + * 生成 flat config 片段。 + * 由调用方注入 plugin 实例,避免 configs ↔ index 循环依赖。 + */ +export function createFlatConfig(plugin, ruleSeverities, name = "@sfmc-bds/eslint-plugin/recommended") { + return { + name, + plugins: { + "@sfmc-bds": plugin, + }, + rules: ruleSeverities, + }; +} diff --git a/modules/sdk/@sfmc-eslint-plugin/dist/index.d.ts b/modules/sdk/@sfmc-eslint-plugin/dist/index.d.ts index 624093ab..0b7b92e0 100644 --- a/modules/sdk/@sfmc-eslint-plugin/dist/index.d.ts +++ b/modules/sdk/@sfmc-eslint-plugin/dist/index.d.ts @@ -1,5 +1,9 @@ import type { ESLint } from "eslint"; import { allRules, recommendedRules, rules } from "./configs/recommended.js"; +/** + * ESLint.Plugin.rules 期望核心 RuleDefinition,而 @typescript-eslint 的 RuleModule + * 在 create/context 形状上更窄;运行时兼容,此处用断言对齐发布面契约(LSP 边界)。 + */ declare const plugin: ESLint.Plugin; export default plugin; export { allRules, recommendedRules, rules }; diff --git a/modules/sdk/@sfmc-eslint-plugin/dist/index.js b/modules/sdk/@sfmc-eslint-plugin/dist/index.js index 17bbf057..e732ac10 100644 --- a/modules/sdk/@sfmc-eslint-plugin/dist/index.js +++ b/modules/sdk/@sfmc-eslint-plugin/dist/index.js @@ -1,4 +1,8 @@ import { allRules, createFlatConfig, recommendedRules, rules, } from "./configs/recommended.js"; +/** + * ESLint.Plugin.rules 期望核心 RuleDefinition,而 @typescript-eslint 的 RuleModule + * 在 create/context 形状上更窄;运行时兼容,此处用断言对齐发布面契约(LSP 边界)。 + */ const plugin = { meta: { name: "@sfmc-bds/eslint-plugin", diff --git a/modules/sdk/@sfmc-eslint-plugin/src/configs/recommended.ts b/modules/sdk/@sfmc-eslint-plugin/src/configs/recommended.ts new file mode 100644 index 00000000..3cf1ae75 --- /dev/null +++ b/modules/sdk/@sfmc-eslint-plugin/src/configs/recommended.ts @@ -0,0 +1,48 @@ +import type { ESLint, Linter } from "eslint"; +import { noPlayerSendMessage } from "../rules/no-player-send-message.js"; +import { noSdkDeepImport } from "../rules/no-sdk-deep-import.js"; +import { noSfmcSdkAlias } from "../rules/no-sfmc-sdk-alias.js"; +import { requireModuleRegistry } from "../rules/require-module-registry.js"; + +/** + * 规则实现注册表(权威来源)。 + * 新增规则只需:实现 rule → 挂到此处 → 写入 recommended/all 严重级别。 + */ +export const rules = { + "no-player-send-message": noPlayerSendMessage, + "no-sfmc-sdk-alias": noSfmcSdkAlias, + "no-sdk-deep-import": noSdkDeepImport, + "require-module-registry": requireModuleRegistry, +}; + +/** recommended:日常模块/SDK 约定 */ +export const recommendedRules: Linter.RulesRecord = { + "@sfmc-bds/no-player-send-message": "warn", + "@sfmc-bds/no-sfmc-sdk-alias": "error", + "@sfmc-bds/no-sdk-deep-import": "error", + "@sfmc-bds/require-module-registry": "warn", +}; + +/** all:在 recommended 上将 no-player-send-message 升为 error */ +export const allRules: Linter.RulesRecord = { + ...recommendedRules, + "@sfmc-bds/no-player-send-message": "error", +}; + +/** + * 生成 flat config 片段。 + * 由调用方注入 plugin 实例,避免 configs ↔ index 循环依赖。 + */ +export function createFlatConfig( + plugin: ESLint.Plugin, + ruleSeverities: Linter.RulesRecord, + name = "@sfmc-bds/eslint-plugin/recommended" +): Linter.Config { + return { + name, + plugins: { + "@sfmc-bds": plugin, + }, + rules: ruleSeverities, + }; +} diff --git a/modules/sdk/@sfmc-eslint-plugin/src/index.ts b/modules/sdk/@sfmc-eslint-plugin/src/index.ts index 3937ba49..1a658539 100644 --- a/modules/sdk/@sfmc-eslint-plugin/src/index.ts +++ b/modules/sdk/@sfmc-eslint-plugin/src/index.ts @@ -6,13 +6,17 @@ import { rules, } from "./configs/recommended.js"; -const plugin: ESLint.Plugin = { +/** + * ESLint.Plugin.rules 期望核心 RuleDefinition,而 @typescript-eslint 的 RuleModule + * 在 create/context 形状上更窄;运行时兼容,此处用断言对齐发布面契约(LSP 边界)。 + */ +const plugin = { meta: { name: "@sfmc-bds/eslint-plugin", version: "0.1.0", }, rules, -}; +} as unknown as ESLint.Plugin; plugin.configs = { recommended: createFlatConfig(plugin, recommendedRules), diff --git a/package-lock.json b/package-lock.json index 653f174b..7f18f1ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,6 +55,7 @@ "../sfmc-modules/packages/economy": { "name": "@sfmc-bds/module-economy", "version": "0.1.0", + "extraneous": true, "dependencies": { "@sfmc-bds/sdk": "*" } @@ -96,6 +97,7 @@ "modules/packages/land": { "name": "@sfmc-bds/module-land", "version": "0.1.0", + "extraneous": true, "dependencies": { "@sfmc-bds/module-economy": "file:../../../../sfmc-modules/packages/economy", "@sfmc-bds/sdk": "*" @@ -885,14 +887,6 @@ "resolved": "modules/packages/afk", "link": true }, - "node_modules/@sfmc-bds/module-economy": { - "resolved": "../sfmc-modules/packages/economy", - "link": true - }, - "node_modules/@sfmc-bds/module-land": { - "resolved": "modules/packages/land", - "link": true - }, "node_modules/@sfmc-bds/qq-bridge": { "resolved": "qq-bridge", "link": true