type StateSetter = (next: T | ((previous: T) => T)) => void; export type ReactLike = { createElement: (...args: any[]) => any; useEffect?: (effect: () => void | (() => void), deps: readonly unknown[]) => void; useState?: (initialState: T | (() => T)) => [T, StateSetter]; }; export type SCUMPageContext = { pageKey?: string; pageTitle?: string; serverInstanceId?: string; permissions: string[]; availability: { available: boolean; reason?: string }; featureAvailability?: Array<{ key: string; available: boolean; reason?: string }>; workspaceActions?: SCUMWorkspaceActions; }; type SCUMWorkspaceActions = { listSCUMUsers?: () => Promise; listSCUMPlayers?: () => Promise; listSCUMSquads?: () => Promise; listSCUMSquadMembers?: () => Promise; listSCUMVehicles?: () => Promise; listSCUMFlags?: () => Promise; listSCUMPositions?: () => Promise; listSCUMMapPoints?: () => Promise; listSCUMGifts?: () => Promise; getSCUMMapGeometry?: () => unknown; listSCUMOperations?: () => Promise; createSCUMOperation?: (request: unknown) => Promise; listSCUMWorkflows?: () => Promise; createSCUMWorkflow?: (request: unknown) => Promise; listSCUMWorkflowSteps?: (workflowId?: string) => Promise; listGameGiftCatalogs?: () => Promise; saveGameGiftCatalog?: (request: unknown) => Promise; publishGameGiftCatalog?: (catalogId: string) => Promise; listGameGiftGrants?: () => Promise; requestGameGiftGrant?: (request: unknown) => Promise; approveGameGiftGrant?: (grantId: string) => Promise; }; type RecordMap = Record; type DataState = { status: "loading" } | { status: "error"; reason: string } | { status: "ready"; data: SCUMSurfaceData }; type ActionState = { status: "idle" | "pending" | "ok" | "error"; message?: string }; type SCUMSurfaceData = { players: RecordMap[]; squads: RecordMap[]; members: RecordMap[]; vehicles: RecordMap[]; flags: RecordMap[]; positions: RecordMap[]; mapPoints: RecordMap[]; giftEvents: RecordMap[]; catalogs: RecordMap[]; grants: RecordMap[]; operations: RecordMap[]; workflows: RecordMap[]; steps: RecordMap[]; mapGeometry?: RecordMap }; const emptyData: SCUMSurfaceData = { players: [], squads: [], members: [], vehicles: [], flags: [], positions: [], mapPoints: [], giftEvents: [], catalogs: [], grants: [], operations: [], workflows: [], steps: [] }; export function renderSCUMFeaturePage(react: ReactLike, input: SCUMPageContext) { const e = react.createElement; const [state, setState] = usePluginState(react, { status: "loading" }); const [action, setAction] = usePluginState(react, { status: "idle" }); const [playerQuery, setPlayerQuery] = usePluginState(react, ""); const [squadFilter, setSquadFilter] = usePluginState(react, ""); const [selectedSquad, setSelectedSquad] = usePluginState(react, ""); const [layers, setLayers] = usePluginState>(react, { player: true, vehicle: true, base: true, flag: true }); const [selectedMarker, setSelectedMarker] = usePluginState(react, undefined); const [giftTab, setGiftTab] = usePluginState(react, "catalogs"); const pageKey = input.pageKey ?? "players"; const refresh = () => { const actions = input.workspaceActions; if (!input.serverInstanceId || !actions) { setState({ status: "error", reason: "插件页面没有绑定服务器,无法读取 SCUM 投影。" }); return; } setState({ status: "loading" }); void Promise.all([ safeList(actions.listSCUMPlayers), safeList(actions.listSCUMSquads), safeList(actions.listSCUMSquadMembers), safeList(actions.listSCUMVehicles), safeList(actions.listSCUMFlags), safeList(actions.listSCUMPositions), safeList(actions.listSCUMMapPoints), safeList(actions.listSCUMGifts), safeList(actions.listGameGiftCatalogs), safeList(actions.listGameGiftGrants), safeList(actions.listSCUMOperations), safeList(actions.listSCUMWorkflows), safeList(actions.listSCUMWorkflowSteps) ]).then(([players, squads, members, vehicles, flags, positions, mapPoints, giftEvents, catalogs, grants, operations, workflows, steps]) => setState({ status: "ready", data: { players, squads, members, vehicles, flags, positions, mapPoints, giftEvents, catalogs, grants, operations, workflows, steps, mapGeometry: asRecord(actions.getSCUMMapGeometry?.()) } })) .catch((error) => setState({ status: "error", reason: error instanceof Error ? error.message : "SCUM 投影读取失败。" })); }; if (react.useEffect) react.useEffect(() => { refresh(); return undefined; }, [input.serverInstanceId, pageKey, input.workspaceActions]); const data = state.status === "ready" ? state.data : emptyData; return e("section", { className: "console-panel", "aria-label": input.pageTitle ?? surfaceTitle(pageKey) }, e("div", { className: "panel-header" }, e("div", null, e("h2", null, input.pageTitle ?? surfaceTitle(pageKey)), e("p", { className: "provider-id" }, surfaceSummary(pageKey))), e("div", { className: "console-row-actions" }, e("span", { className: "page-status" }, input.availability.available ? "投影/Companion 可用" : input.availability.reason ?? "等待 Run/Companion"), e("button", { type: "button", className: "icon-command", onClick: refresh }, "刷新投影"), workflowButton(e, input, setAction, refresh, pageWorkflow(pageKey)) ) ), action.status !== "idle" ? e("p", { className: "page-status", "data-state": action.status }, action.message) : null, state.status === "loading" ? e("p", { className: "page-status" }, "正在读取平台本地 SCUM 投影…") : null, state.status === "error" ? e("p", { className: "page-status", "data-state": "error" }, state.reason) : null, state.status === "ready" ? renderSurfaceBody(e, pageKey, data, input, setAction, refresh, { playerQuery, setPlayerQuery, squadFilter, setSquadFilter, selectedSquad, setSelectedSquad, layers, setLayers, selectedMarker, setSelectedMarker, giftTab, setGiftTab }) : null ); } type SurfaceControls = { playerQuery: string; setPlayerQuery: StateSetter; squadFilter: string; setSquadFilter: StateSetter; selectedSquad: string; setSelectedSquad: StateSetter; layers: Record; setLayers: StateSetter>; selectedMarker?: RecordMap; setSelectedMarker: StateSetter; giftTab: string; setGiftTab: StateSetter }; function renderSurfaceBody(e: ReactLike["createElement"], pageKey: string, data: SCUMSurfaceData, input: SCUMPageContext, setAction: StateSetter, refresh: () => void, controls: SurfaceControls) { switch (pageKey) { case "players": return playersSurface(e, data, input, setAction, refresh, controls); case "squads": return squadsSurface(e, data, controls); case "live-map": return mapSurface(e, data, controls); case "gifts": return giftsSurface(e, data, input, setAction, refresh, controls); case "workflows": return workflowsSurface(e, data); default: return playersSurface(e, data, input, setAction, refresh, controls); } } function playersSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, input: SCUMPageContext, setAction: StateSetter, refresh: () => void, controls: SurfaceControls) { const squads = uniqueStrings(data.players.map((player) => textField(player, "SquadName", "squadName") || textField(player, "SquadID", "squadId"))); const query = controls.playerQuery.trim().toLowerCase(); const players = data.players.filter((player) => (!controls.squadFilter || controls.squadFilter === (textField(player, "SquadName", "squadName") || textField(player, "SquadID", "squadId"))) && (!query || ["DisplayName", "displayName", "SteamID", "steamId", "SquadName", "squadName"].some((key) => textField(player, key).toLowerCase().includes(query)))); return e("div", { className: "console-record-list" }, statsStrip(e, [["玩家投影", data.players.length], ["在线", data.players.filter((p) => boolField(p, "Online", "online")).length], ["坐标", data.positions.length], ["待审操作", data.operations.filter((op) => field(op, "Status", "status") === "waiting").length]]), e("div", { className: "server-toolbar" }, e("input", { type: "search", value: controls.playerQuery, placeholder: "搜索昵称、Steam 或队伍", onChange: (event: { target: { value: string } }) => controls.setPlayerQuery(event.target.value) }), e("select", { value: controls.squadFilter, onChange: (event: { target: { value: string } }) => controls.setSquadFilter(event.target.value) }, e("option", { value: "" }, "全部队伍"), squads.map((squad) => e("option", { key: squad, value: squad }, squad)))), players.length ? players.slice(0, 80).map((player) => e("article", { key: idOf(player), className: "console-record" }, e("div", { className: "console-record-head" }, e("strong", null, textField(player, "DisplayName", "displayName") || textField(player, "GamePlayerID", "gamePlayerId") || "未知玩家"), e("span", { className: `status-pill ${boolField(player, "Online", "online") ? "status-active" : "status-disabled"}` }, boolField(player, "Online", "online") ? "在线" : "离线/未知")), e("div", { className: "console-record-meta" }, e("span", null, `Steam ${textField(player, "SteamID", "steamId") || "unknown"}`), e("span", null, `Profile ${textField(player, "UserProfileID", "userProfileId") || "unknown"}`), e("span", null, `队伍 ${textField(player, "SquadName", "squadName") || textField(player, "SquadID", "squadId") || "unknown"}`), e("span", null, freshness(player))), e("span", { className: "provider-id" }, `Fame ${numField(player, "FamePoints", "famePoints")} · Cash ${numField(player, "NormalBalance", "normalBalance")} · Gold ${numField(player, "GoldBalance", "goldBalance")} · ${coords(field(player, "Position", "position") as RecordMap | undefined)}`), e("div", { className: "console-row-actions" }, operationButton(e, input, setAction, refresh, player, "player.fame.set", "fame", "Fame +100", 100), operationButton(e, input, setAction, refresh, player, "player.currency.normal.set", "amount", "现金 +1000", 1000), operationButton(e, input, setAction, refresh, player, "player.attribute.855.set", "after", "855 审批", Number(numField(player, "855", "855")) || 1, true) ) )) : e("p", { className: "page-status" }, data.players.length ? "没有符合筛选条件的真实玩家投影。" : "暂无玩家投影。先运行 player/world refresh workflow;不会显示假玩家。") ); } function squadsSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, controls: SurfaceControls) { const selected = controls.selectedSquad || textField(data.squads[0], "SquadID", "squadId"); const roster = data.members.filter((member) => textField(member, "SquadID", "squadId") === selected); const flags = data.flags.filter((flag) => textField(flag, "OwnerSquadID", "ownerSquadId") === selected); const squadRows = e("div", { className: "console-row-list" }, data.squads.map((squad) => { const id = textField(squad, "SquadID", "squadId"); return e("button", { type: "button", key: idOf(squad), className: `console-row ${selected === id ? "console-row-selected" : ""}`, onClick: () => controls.setSelectedSquad(id) }, e("span", null, textField(squad, "Name", "name") || id), e("strong", null, `成员 ${numField(squad, "MemberCount", "memberCount")}`), e("strong", null, `分数 ${numField(squad, "Score", "score")}`), e("strong", null, freshness(squad))); })); const rosterRows = roster.length ? e("div", { className: "console-row-list" }, roster.map((member) => e("div", { key: idOf(member), className: "console-row" }, e("span", null, textField(member, "DisplayName", "displayName") || "未知成员"), e("strong", null, textField(member, "IsLeader", "isLeader") === "true" ? "队长" : `军衔 ${textField(member, "Rank", "rank") || "--"}`), e("strong", null, `Steam ${textField(member, "SteamID", "steamId") || "--"}`), e("strong", null, freshness(member))))) : e("p", { className: "page-status" }, "此队伍暂无成员投影。"); const flagRows = e("div", { className: "console-row-list" }, flags.map((flag) => e("div", { key: idOf(flag), className: "console-row" }, e("span", null, `旗帜 ${textField(flag, "FlagID", "flagId")}`), e("strong", null, textField(flag, "OwnershipConfidence", "ownershipConfidence") || "verified"), e("strong", null, coords(field(flag, "Position", "position") as RecordMap)), e("strong", null, freshness(flag))))); return e("div", { className: "overview-two-col" }, e("article", { className: "console-module" }, e("div", { className: "panel-header" }, e("h2", null, "队伍"), e("span", { className: "page-status" }, `${data.squads.length} 条`)), squadRows), e("article", { className: "console-module" }, e("div", { className: "panel-header" }, e("h2", null, "成员 / 旗帜"), e("span", { className: "page-status" }, selected || "未选择队伍")), rosterRows, flagRows)); } function mapSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, controls: SurfaceControls) { const overlays = mapOverlays(data); const visible = overlays.filter((point) => controls.layers[textField(point, "SubjectType", "subjectType")] !== false); return e("div", { className: "console-record-list" }, statsStrip(e, [["玩家", overlays.filter((point) => textField(point, "SubjectType", "subjectType") === "player").length], ["载具", overlays.filter((point) => textField(point, "SubjectType", "subjectType") === "vehicle").length], ["旗帜", overlays.filter((point) => textField(point, "SubjectType", "subjectType") === "flag").length], ["坐标点", visible.length]]), e("div", { className: "map-layer-controls" }, ["player", "vehicle", "base", "flag"].map((kind) => e("label", { key: kind }, e("input", { type: "checkbox", checked: controls.layers[kind] !== false, onChange: () => controls.setLayers((previous) => ({ ...previous, [kind]: previous[kind] === false })) }), mapKindLabel(kind)))), e("div", { className: "map-projection-board" }, visible.slice(0, 500).map((point, index) => e("button", { type: "button", key: `${idOf(point)}:${index}`, className: `map-projection-dot map-projection-dot-${textField(point, "SubjectType", "subjectType") || "unknown"}`, title: `${mapKindLabel(textField(point, "SubjectType", "subjectType"))} ${coords(point)}`, style: dotStyle(point, data.mapGeometry), onClick: () => controls.setSelectedMarker(point) }, ""))), controls.selectedMarker ? e("article", { className: "console-record" }, e("div", { className: "console-record-head" }, e("strong", null, mapKindLabel(textField(controls.selectedMarker, "SubjectType", "subjectType"))), e("span", { className: "status-pill status-active" }, freshness(controls.selectedMarker))), e("div", { className: "console-record-meta" }, e("span", null, `对象 ${textField(controls.selectedMarker, "SubjectID", "subjectId") || "--"}`), e("span", null, coords(controls.selectedMarker)), e("span", null, `观察 ${dateField(controls.selectedMarker, "ObservedAt", "observedAt")}`))) : null, tablePanel(e, "地图覆盖物", visible, (point) => [mapKindLabel(textField(point, "SubjectType", "subjectType")), textField(point, "Label", "label") || textField(point, "SubjectID", "subjectId") || textField(point, "GamePlayerID", "gamePlayerId") || textField(point, "VehicleID", "vehicleId") || "unknown", coords(point), freshness(point)]) ); } function giftsSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, input: SCUMPageContext, setAction: StateSetter, refresh: () => void, controls: SurfaceControls) { const tab = controls.giftTab; return e("div", { className: "console-record-list" }, statsStrip(e, [["礼包定义", (data.catalogs ?? []).length], ["发放单", (data.grants ?? []).length], ["定时礼包完成", (data.giftEvents ?? []).length], ["可选玩家", data.players.length]]), e("p", { className: "page-status" }, "礼包只走 typed delivery workflow;结果未知时不会重复发放。"), e("div", { className: "section-tabs" }, [["catalogs", "礼包定义"], ["grants", "发放记录"], ["events", "定时礼包完成"]].map(([key, label]) => e("button", { type: "button", key, className: `section-tab ${tab === key ? "section-tab-active" : ""}`, onClick: () => controls.setGiftTab(key) }, label))), tab === "catalogs" ? giftCatalogSurface(e, data, input, setAction, refresh) : null, tab === "grants" ? giftGrantSurface(e, data, input, setAction, refresh) : null, tab === "events" ? tablePanel(e, "游戏内已完成定时礼包", data.giftEvents ?? [], (event) => [textField(event, "displayName", "DisplayName") || "未知玩家", textField(event, "giftType", "GiftType") || "finished-timed-gift", dateField(event, "spawnAt", "SpawnAt"), textField(event, "mapId", "MapID") || "--"]) : null ); } function giftCatalogSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, input: SCUMPageContext, setAction: StateSetter, refresh: () => void) { const catalogs = data.catalogs ?? []; return e("div", { className: "console-row-list" }, catalogs.length ? catalogs.map((catalog) => e("article", { key: idOf(catalog), className: "console-record" }, e("div", { className: "console-record-head" }, e("strong", null, textField(catalog, "Name", "name")), e("span", { className: "status-pill status-active" }, textField(catalog, "GameVersion", "gameVersion"))), e("span", { className: "provider-id" }, giftItemsText(field(catalog, "DraftItems", "draftItems") as RecordMap[])), e("div", { className: "console-row-actions" }, e("button", { type: "button", className: "icon-command", onClick: () => publishCatalog(input, setAction, refresh, idOf(catalog)) }, "发布版本")))) : e("p", { className: "page-status" }, "暂无平台运营礼包定义。")); } function giftGrantSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, input: SCUMPageContext, setAction: StateSetter, refresh: () => void) { const grants = data.grants ?? []; return e("div", { className: "console-row-list" }, grants.length ? grants.map((grant) => e("article", { key: idOf(grant), className: "console-record" }, e("div", { className: "console-record-head" }, e("strong", null, textField(grant, "PlayerDisplayName", "playerDisplayName") || "未知玩家"), e("span", { className: "status-pill status-disabled" }, textField(grant, "Status", "status"))), e("span", { className: "provider-id" }, giftItemsText(field(grant, "Items", "items") as RecordMap[])), textField(grant, "Status", "status") === "pending-approval" ? e("div", { className: "console-row-actions" }, e("button", { type: "button", className: "icon-command", onClick: () => approveGrant(input, setAction, refresh, idOf(grant)) }, "批准发放")) : null)) : e("p", { className: "page-status" }, "暂无礼包发放记录。")); } function workflowsSurface(e: ReactLike["createElement"], data: SCUMSurfaceData) { return e("div", { className: "console-record-list" }, data.workflows.length ? data.workflows.map((wf) => e("article", { key: idOf(wf), className: "console-record" }, e("div", { className: "console-record-head" }, e("strong", null, textField(wf, "TemplateKey", "templateKey") || idOf(wf)), e("span", { className: "status-pill status-active" }, textField(wf, "Status", "status") || "queued")), e("div", { className: "console-record-meta" }, e("span", null, `当前步骤 ${textField(wf, "CurrentStepKey", "currentStepKey") || "等待调度"}`), e("span", null, `创建 ${dateField(wf, "CreatedAt", "createdAt")}`)), e("span", { className: "provider-id" }, summaryText(wf)) )) : e("p", { className: "page-status" }, "暂无 workflow。可以从各页面发起 refresh/audit/correction/gift workflow。"), tablePanel(e, "步骤", data.steps, (step) => [textField(step, "StepKey", "stepKey"), textField(step, "Status", "status"), textField(step, "Capability", "capability") || textField(step, "QueryTemplateKey", "queryTemplateKey") || textField(step, "OperationKey", "operationKey"), summaryText(step)]) ); } function workflowButton(e: ReactLike["createElement"], input: SCUMPageContext, setAction: StateSetter, refresh: () => void, templateKey: string) { if (!templateKey) return null; return e("button", { type: "button", className: "primary-command", disabled: !input.workspaceActions?.createSCUMWorkflow, onClick: () => createWorkflow(input, setAction, refresh, templateKey) }, workflowLabel(templateKey)); } function operationButton(e: ReactLike["createElement"], input: SCUMPageContext, setAction: StateSetter, refresh: () => void, player: RecordMap, templateKey: string, valueKey: string, label: string, value: unknown, guarded = false) { return e("button", { type: "button", className: "icon-command", disabled: !input.workspaceActions?.createSCUMOperation, onClick: () => createOperation(input, setAction, refresh, player, templateKey, valueKey, value, guarded) }, label); } function createWorkflow(input: SCUMPageContext, setAction: StateSetter, refresh: () => void, templateKey: string) { setAction({ status: "pending", message: `正在创建 ${templateKey} workflow…` }); void input.workspaceActions?.createSCUMWorkflow?.({ templateKey, idempotencyKey: `plugin:${templateKey}:${input.serverInstanceId}:${Date.now()}` }).then((result) => { setAction({ status: "ok", message: `Workflow 已创建:${textField(result as RecordMap, "id") || templateKey}` }); refresh(); }).catch((error) => setAction({ status: "error", message: error instanceof Error ? error.message : "Workflow 创建失败。" })); } function createOperation(input: SCUMPageContext, setAction: StateSetter, refresh: () => void, player: RecordMap, templateKey: string, valueKey: string, value: unknown, guarded: boolean) { const playerId = textField(player, "GamePlayerID", "gamePlayerId") || textField(player, "SteamID", "steamId"); const before = guarded ? field(field(player, "UnknownFields", "unknownFields") as RecordMap | undefined, "855") ?? 0 : undefined; const payload: RecordMap = guarded ? { fieldKey: "855", before, after: value, safetyWindow: `plugin-maintenance-${Date.now()}`, backupRef: `backup-required:${Date.now()}` } : { [valueKey]: value }; setAction({ status: "pending", message: `正在创建 ${templateKey} typed operation…` }); void input.workspaceActions?.createSCUMOperation?.({ templateKey, playerId, payload, reason: "SCUM plugin projection surface request", idempotencyKey: `plugin:${templateKey}:${playerId}:${Date.now()}` }).then((result) => { setAction({ status: "ok", message: `操作已进入审批/确认队列:${textField(result as RecordMap, "id") || templateKey}` }); refresh(); }).catch((error) => setAction({ status: "error", message: error instanceof Error ? error.message : "操作创建失败。" })); } function pageWorkflow(pageKey: string): string { switch (pageKey) { case "players": return "scum.player-refresh"; case "squads": return "scum.territory-audit"; case "live-map": return "scum.world-refresh"; case "gifts": return "scum.gift-delivery"; case "workflows": return "scum.product-cleanup"; default: return "scum.bootstrap-real-data"; } } function surfaceTitle(pageKey: string): string { return pageKey === "squads" ? "队伍/旗帜管理" : pageKey === "live-map" ? "实时地图" : pageKey === "gifts" ? "礼包管理" : pageKey === "workflows" ? "Workflow 状态" : "用户管理"; } function surfaceSummary(pageKey: string): string { return pageKey === "live-map" ? "玩家、载具、旗帜坐标来自平台本地投影;缺失时显示 stale/unknown。" : pageKey === "gifts" ? "礼包发放、通知和确认都通过 typed workflow,不直接改投影。" : pageKey === "squads" ? "队伍、成员、旗帜所有权来自 SCUM.db typed observations。" : "玩家列表由登录日志和 SCUM.db typed observations 创建,不显示样例数据。"; } function workflowLabel(templateKey: string): string { return templateKey.includes("audit") ? "发起审计" : templateKey.includes("gift") ? "创建发放 workflow" : templateKey.includes("world") ? "刷新世界投影" : templateKey.includes("cleanup") ? "清理旧入口" : "刷新真实数据"; } function statsStrip(e: ReactLike["createElement"], items: Array<[string, number]>) { return e("div", { className: "console-stat-strip" }, items.map(([label, value]) => e("span", { key: label, className: "server-card-stat" }, e("span", null, label), e("strong", null, String(value))))); } function tablePanel(e: ReactLike["createElement"], title: string, rows: RecordMap[], render: (row: RecordMap) => unknown[]) { return e("article", { className: "console-module" }, e("div", { className: "panel-header" }, e("h2", null, title), e("span", { className: "page-status" }, `${rows.length} 条`)), e("div", { className: "console-row-list" }, rows.length ? rows.slice(0, 100).map((row) => e("div", { key: idOf(row), className: "console-row" }, render(row).map((part, i) => i === 0 ? e("span", { key: i }, String(part ?? "unknown")) : e("strong", { key: i }, String(part ?? "unknown"))))) : e("p", { className: "page-status" }, "暂无真实投影数据。"))); } function mapOverlays(data: SCUMSurfaceData): RecordMap[] { const mapPoints = (data.mapPoints ?? data.positions ?? []).map((point) => ({ ...(field(point, "Fields", "fields") as RecordMap), ...point, SubjectType: textField(field(point, "Fields", "fields") as RecordMap, "subjectType", "SubjectType") || textField(point, "SubjectType", "subjectType") || "player" })); if (mapPoints.length) return mapPoints; const vehicles = data.vehicles.map((vehicle) => ({ ...(field(vehicle, "Position", "position") as RecordMap), SubjectType: "vehicle", Label: textField(vehicle, "Label", "label") || textField(vehicle, "ClassName", "className"), VehicleID: textField(vehicle, "VehicleID", "vehicleId") })).filter((point) => Boolean(point)); const flags = data.flags.map((flag) => ({ ...(field(flag, "Position", "position") as RecordMap), SubjectType: "flag", Label: textField(flag, "OwnerSquadName", "ownerSquadName"), FlagID: textField(flag, "FlagID", "flagId") })).filter((point) => Boolean(point)); return [...mapPoints, ...vehicles, ...flags]; } function dotStyle(point: RecordMap, geometry?: RecordMap): Record { const x = Number(field(point, "X", "x") ?? 0); const y = Number(field(point, "Y", "y") ?? 0); const minX = Number(field(geometry, "WorldMinX", "worldMinX")); const maxX = Number(field(geometry, "WorldMaxX", "worldMaxX")); const minY = Number(field(geometry, "WorldMinY", "worldMinY")); const maxY = Number(field(geometry, "WorldMaxY", "worldMaxY")); if ([minX, maxX, minY, maxY].every(Number.isFinite) && maxX > minX && maxY > minY) return { left: `${Math.max(1, Math.min(99, (x - minX) / (maxX - minX) * 100))}%`, top: `${Math.max(1, Math.min(99, 100 - (y - minY) / (maxY - minY) * 100))}%` }; return { left: "50%", top: "50%" }; } function mapKindLabel(kind: string): string { return kind === "player" ? "玩家" : kind === "vehicle" ? "载具" : kind === "base" ? "基地" : kind === "flag" ? "旗帜" : "实体"; } function uniqueStrings(values: string[]): string[] { return [...new Set(values.filter(Boolean))].sort((a, b) => a.localeCompare(b, "zh-CN")); } function asRecord(value: unknown): RecordMap | undefined { return value && typeof value === "object" ? value as RecordMap : undefined; } function giftItemsText(items: RecordMap[] | undefined): string { return Array.isArray(items) && items.length ? items.map((item) => `${textField(item, "Label", "label", "CatalogItemKey", "catalogItemKey")} x${textField(item, "Quantity", "quantity")}`).join(" · ") : "未声明物品"; } function publishCatalog(input: SCUMPageContext, setAction: StateSetter, refresh: () => void, catalogId: string) { setAction({ status: "pending", message: "正在发布礼包版本…" }); void input.workspaceActions?.publishGameGiftCatalog?.(catalogId).then(() => { setAction({ status: "ok", message: "礼包版本已发布。" }); refresh(); }).catch((error) => setAction({ status: "error", message: error instanceof Error ? error.message : "礼包发布失败。" })); } function approveGrant(input: SCUMPageContext, setAction: StateSetter, refresh: () => void, grantId: string) { setAction({ status: "pending", message: "正在批准礼包发放…" }); void input.workspaceActions?.approveGameGiftGrant?.(grantId).then(() => { setAction({ status: "ok", message: "礼包已进入发放队列。" }); refresh(); }).catch((error) => setAction({ status: "error", message: error instanceof Error ? error.message : "礼包批准失败。" })); } function safeList(fn?: () => Promise): Promise { return fn ? fn().then((value) => Array.isArray((value as RecordMap)?.items) ? (value as { items: RecordMap[] }).items : []) : Promise.resolve([]); } function usePluginState(react: ReactLike, initial: T): [T, StateSetter] { return react.useState ? react.useState(initial) : [initial, () => undefined]; } function field(row: RecordMap | undefined, ...keys: string[]): unknown { if (!row) return undefined; for (const key of keys) if (row[key] !== undefined) return row[key]; return undefined; } function textField(row: RecordMap | unknown, ...keys: string[]): string { const value = field(row as RecordMap, ...keys); return value === undefined || value === null ? "" : String(value); } function boolField(row: RecordMap, ...keys: string[]): boolean { const value = field(row, ...keys); return value === true || value === "true"; } function numField(row: RecordMap, ...keys: string[]): string { const value = field(row, ...keys); return value === undefined || value === null || value === "" ? "--" : String(value); } function idOf(row: RecordMap): string { return textField(row, "ID", "id", "GamePlayerID", "gamePlayerId", "SquadID", "squadId", "VehicleID", "vehicleId", "FlagID", "flagId", "StepKey", "stepKey") || Math.random().toString(36).slice(2); } function freshness(row: RecordMap): string { const fresh = field(row, "Freshness", "freshness") as RecordMap | undefined; return textField(fresh, "Status", "status") || "unknown"; } function coords(row?: RecordMap): string { if (!row) return "坐标 unknown"; const ok = field(row, "HasCoordinates", "hasCoordinates"); return ok === false ? "坐标 unknown" : `X ${numField(row, "X", "x")} / Y ${numField(row, "Y", "y")} / Z ${numField(row, "Z", "z")}`; } function summaryText(row: RecordMap): string { const summary = field(row, "SafeSummary", "safeSummary") as RecordMap | undefined; return textField(summary, "Message", "message") || textField(row, "BlockerReason", "blockerReason") || "safe summary pending"; } function dateField(row: RecordMap, ...keys: string[]): string { const value = textField(row, ...keys); return value ? new Date(value).toLocaleString() : "unknown"; }