Remove SCUM server-management client path
This commit is contained in:
@@ -1,23 +0,0 @@
|
||||
import type { SCUMCommandResult, SCUMConfigPatch, SCUMConfigRead, SCUMFeatureAvailability, SCUMFeatureKey, SCUMStatePatch, SCUMStateSnapshot, SCUMVehicleSpawn } from "./contracts.js";
|
||||
import { validateConfigPatch, validateStatePatch, validateVehicleSpawn } from "./schemas.js";
|
||||
|
||||
export type PluginFeatureBridge = { dispatch(action: "game-client.command", payload: Record<string, string>): Promise<{ status: string; result?: Record<string, string>; error?: { message: string } }> };
|
||||
export type SCUMFeatureAPI = {
|
||||
availability(feature: SCUMFeatureKey): Promise<SCUMFeatureAvailability>; readConfig(): Promise<SCUMConfigRead | null>; patchConfig(patch: SCUMConfigPatch): Promise<SCUMCommandResult>;
|
||||
stateSnapshot(playerId: string): Promise<SCUMStateSnapshot | null>; requestStatePatch(patch: SCUMStatePatch): Promise<SCUMCommandResult>; requestVehicleSpawn(spawn: SCUMVehicleSpawn): Promise<SCUMCommandResult>;
|
||||
};
|
||||
|
||||
export function createSCUMFeatureAPI(bridge: PluginFeatureBridge, availableFeatures: readonly SCUMFeatureAvailability[]): SCUMFeatureAPI {
|
||||
const availability = async (feature: SCUMFeatureKey) => availableFeatures.find((item) => item.feature === feature) ?? { feature, available: false, reason: "插件未声明此功能。" };
|
||||
return {
|
||||
availability,
|
||||
async readConfig() { const result = await bridge.dispatch("game-client.command", { type: "config.read" }); return result.status === "ok" ? decode<SCUMConfigRead>(result.result) : null; },
|
||||
async patchConfig(patch) { const error = validateConfigPatch(patch); if (error) return { status: "validation-failed", summary: error }; return commandResult(await bridge.dispatch("game-client.command", { type: "config.patch", patch: JSON.stringify(patch) })); },
|
||||
async stateSnapshot(playerId) { const result = await bridge.dispatch("game-client.command", { type: "player.lookup", playerId }); return result.status === "ok" ? decode<SCUMStateSnapshot>(result.result) : null; },
|
||||
async requestStatePatch(patch) { const error = validateStatePatch(patch.changes); if (error) return { status: "validation-failed", summary: error }; return commandResult(await bridge.dispatch("game-client.command", { type: "game-state.patch", patch: JSON.stringify(patch) })); },
|
||||
async requestVehicleSpawn(spawn) { const error = validateVehicleSpawn(spawn); if (error) return { status: "validation-failed", summary: error }; return commandResult(await bridge.dispatch("game-client.command", { type: "vehicle.spawn", vehicleCode: spawn.vehicleCode })); }
|
||||
};
|
||||
}
|
||||
|
||||
function commandResult(result: { status: string; result?: Record<string, string>; error?: { message: string } }): SCUMCommandResult { if (result.status === "queued") return { status: "queued", summary: result.result?.summary ?? "已进入任务队列。" }; if (result.status === "unsupported") return { status: "unsupported", summary: result.error?.message ?? "当前运行时不支持此操作。" }; return { status: "failed", summary: result.error?.message ?? "操作未被接受。" }; }
|
||||
function decode<T>(result: Record<string, string> | undefined): T | null { const payload = result?.payload; if (!payload) return null; try { return JSON.parse(payload) as T; } catch { return null; } }
|
||||
@@ -6,8 +6,8 @@ export function pluginOwned<T extends Record<string, unknown>>(payload: T, recor
|
||||
|
||||
// The authority flag is exact-server. Missing, duplicate, or
|
||||
// transitional flags fail closed: history remains readable, but plugin writes
|
||||
// are not enabled. Execution still additionally requires Companion feature
|
||||
// availability; this flag never authorizes a command by itself.
|
||||
// are not enabled. Execution still additionally requires the plugin-declared
|
||||
// feature surface to be available; this flag never authorizes a command by itself.
|
||||
export function migrationStatus(flags: readonly SCUMFeatureMigrationAuthority[], serverInstanceId: string, feature: SCUMFeatureKey): SCUMFeatureMigrationStatus {
|
||||
const matches = flags.filter((flag) => flag.serverInstanceId === serverInstanceId && flag.feature === feature);
|
||||
if (matches.length !== 1) return { authority: "transitional-read-only", readOnlyHistory: true, pluginWritesEnabled: false, reason: matches.length ? "迁移标记冲突,已保持只读。" : "当前服务器尚未启用插件权威记录。" };
|
||||
|
||||
@@ -8,15 +8,6 @@ export type PluginDataActions = {
|
||||
transact: (collection: string, mutations: PluginDataMutation[]) => Promise<unknown>;
|
||||
};
|
||||
|
||||
export type PluginGameClientQueueRequest = {
|
||||
profileKey: string;
|
||||
commandType: string;
|
||||
payload: RecordMap;
|
||||
idempotencyKey: string;
|
||||
priority?: number;
|
||||
expiresAt: string;
|
||||
};
|
||||
|
||||
export type PluginBridgeExecuteEnvelope = { requestId: string; action: string; payload?: Record<string, string> };
|
||||
export type PluginBridgeExecutionResult = { status?: string; result?: Record<string, string>; error?: { message?: string } };
|
||||
|
||||
@@ -82,12 +73,6 @@ export async function queuePlayerAttributePatch(actions: SCUMWorkspaceActions, p
|
||||
|
||||
export type SCUMWorkspaceActions = {
|
||||
pluginData?: PluginDataActions;
|
||||
gameClient?: {
|
||||
queue: (request: PluginGameClientQueueRequest) => Promise<unknown>;
|
||||
get: (commandId: string) => Promise<unknown>;
|
||||
list: (filter?: { profileKey?: string; state?: string; commandType?: string }) => Promise<unknown>;
|
||||
snapshots: (query?: { profileKey?: string; type?: string; streamKey?: string; observedAfter?: string; limit?: number }) => Promise<unknown>;
|
||||
};
|
||||
dispatch?: (envelope: PluginBridgeExecuteEnvelope, signal?: AbortSignal) => Promise<PluginBridgeExecutionResult>;
|
||||
};
|
||||
|
||||
@@ -163,14 +148,6 @@ export async function loadSCUMSurface(actions: SCUMWorkspaceActions, pageKey: st
|
||||
const keys = pageCollections[canonicalPageKey(pageKey)];
|
||||
const records = await Promise.all(keys.map(async (key) => [key, await actions.pluginData!.list(scumCollections[key])] as const));
|
||||
for (const [key, response] of records) data[key] = collectionRecords(response);
|
||||
if (keys.includes("players") && actions.gameClient) {
|
||||
const playersSnapshot = await actions.gameClient.snapshots({ profileKey: "scum-client-manager", type: "players", streamKey: "current", limit: 1 }).catch(() => undefined);
|
||||
data.players = mergePlayerSnapshots(data.players, playersSnapshot);
|
||||
}
|
||||
if (keys.includes("vehicles") && actions.gameClient) {
|
||||
const vehiclesSnapshot = await actions.gameClient.snapshots({ profileKey: "scum-client-manager", type: "vehicles", streamKey: "current", limit: 1 }).catch(() => undefined);
|
||||
data.vehicles = mergeVehicleSnapshots(data.vehicles, vehiclesSnapshot);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -238,7 +215,7 @@ export async function createGiftDelivery(actions: SCUMWorkspaceActions, delivery
|
||||
}
|
||||
|
||||
export async function queueGiftDelivery(actions: SCUMWorkspaceActions, gift: RecordMap, player: RecordMap): Promise<unknown> {
|
||||
if (!actions.gameClient) throw new Error("通用 gameClient 能力不可用。");
|
||||
if (!actions.dispatch) throw new Error("通用 remote.access.request 能力不可用,无法通过 SCUM RCON 发放礼包。");
|
||||
const giftCode = requiredKey(gift, "code", "礼包编号");
|
||||
const playerId = firstText(player, "gamePlayerId", "playerId", "steamId", "id");
|
||||
if (!playerId) throw new Error("用户编号不能为空。");
|
||||
@@ -247,16 +224,15 @@ export async function queueGiftDelivery(actions: SCUMWorkspaceActions, gift: Rec
|
||||
if (!items.length && !operations.length) throw new Error("礼包必须包含物品或命令。");
|
||||
const now = Date.now();
|
||||
const grantId = safeCommandId(`gift:${giftCode}:${playerId}:${now}`);
|
||||
const command = await actions.gameClient.queue({
|
||||
profileKey: "scum-client-manager",
|
||||
commandType: "reward.deliver",
|
||||
payload: { grantId, playerId, items, operations },
|
||||
idempotencyKey: grantId,
|
||||
expiresAt: new Date(now + 5 * 60_000).toISOString()
|
||||
});
|
||||
const record = { id: grantId, giftCode, giftName: textValue(gift.name), playerId, playerName: firstText(player, "displayName", "playerName", "name"), status: "queued", commandId: isRecord(command) ? textValue(command.id) : "", createdAt: new Date(now).toISOString() };
|
||||
const commands = [...items.map((item) => `#SpawnItem ${item.catalogCode} ${item.quantity}`), ...operations].map(normalizeRCONCommand);
|
||||
const results = [];
|
||||
for (const [index, command] of commands.entries()) {
|
||||
results.push(await dispatchSCUMRCONCommand(actions, command, `${grantId}:${index + 1}`));
|
||||
}
|
||||
const commandIds = results.map((result) => textValue(result.result?.jobId)).filter(Boolean);
|
||||
const record = { id: grantId, giftCode, giftName: textValue(gift.name), playerId, playerName: firstText(player, "displayName", "playerName", "name"), status: "queued", commandId: commandIds[0] ?? "", commandIds, createdAt: new Date(now).toISOString() };
|
||||
await createGiftDelivery(actions, record);
|
||||
return command;
|
||||
return { status: "queued", result: { jobIds: commandIds.join(",") } };
|
||||
}
|
||||
|
||||
export async function saveEventDefinition(actions: SCUMWorkspaceActions, event: RecordMap): Promise<unknown> {
|
||||
@@ -282,28 +258,15 @@ export async function deleteEventProduce(actions: SCUMWorkspaceActions, produce:
|
||||
}
|
||||
|
||||
export async function startEvent(actions: SCUMWorkspaceActions, event: RecordMap, produces: RecordMap[] = []): Promise<unknown> {
|
||||
if (!actions.gameClient) throw new Error("通用 gameClient 能力不可用。");
|
||||
if (!actions.dispatch) throw new Error("通用 remote.access.request 能力不可用,无法通过 SCUM RCON 启动活动。");
|
||||
const eventId = requiredKey(event, "id", "活动编号");
|
||||
const eventClass = Number(event.class) === 2 || firstText(event, "eventType") === "fixed" ? 2 : 1;
|
||||
const eventType = eventClass === 2 ? "fixed" : "range";
|
||||
const queuedProduces = normalizeEventProduces(produces);
|
||||
const command = firstText(event, "rconCommand", "command");
|
||||
if (!command) throw new Error("活动未声明可执行的 SCUM RCON 命令。");
|
||||
const now = Date.now();
|
||||
const runId = safeCommandId(`event:${eventId}:${now}`);
|
||||
const command = await actions.gameClient.queue({
|
||||
profileKey: "scum-client-manager",
|
||||
commandType: "event.start",
|
||||
payload: {
|
||||
eventId, eventType, class: eventClass, title: textValue(event.name) || eventId,
|
||||
placard: firstText(event, "placard", "announcement"), percent: boundedInteger(event.percent ?? event.probability, 0, 100, 100),
|
||||
npc: minimumInteger(event.npc, 0, 0), item: minimumInteger(event.item, 0, 0), zombie: minimumInteger(event.zombie, 0, 0), animal: minimumInteger(event.animal, 0, 0),
|
||||
produces: queuedProduces,
|
||||
durationSeconds: minimumInteger(event.durationSeconds, 1, 1800), announce: event.announce !== false
|
||||
},
|
||||
idempotencyKey: runId,
|
||||
expiresAt: new Date(now + 5 * 60_000).toISOString()
|
||||
});
|
||||
await requirePluginData(actions).put(scumCollections.eventRuns, runId, { id: runId, eventId, eventName: textValue(event.name), status: "queued", commandId: isRecord(command) ? textValue(command.id) : "", definition: event, produces, startedAt: new Date(now).toISOString() });
|
||||
return command;
|
||||
const dispatched = await dispatchSCUMRCONCommand(actions, normalizeRCONCommand(command), runId);
|
||||
await requirePluginData(actions).put(scumCollections.eventRuns, runId, { id: runId, eventId, eventName: textValue(event.name), status: "queued", commandId: textValue(dispatched.result?.jobId), definition: event, produces, startedAt: new Date(now).toISOString() });
|
||||
return dispatched;
|
||||
}
|
||||
|
||||
export function parseGiftItems(input: string): Array<{ catalogCode: string; quantity: number }> {
|
||||
@@ -414,13 +377,6 @@ function requiredKey(value: RecordMap, key: string, label: string): string {
|
||||
|
||||
function requiredRecordKey(value: RecordMap, label: string): string { const key = firstText(value, "_recordKey", "id", "produceId"); if (!key) throw new Error(`${label}编号不能为空。`); return key.includes(":") ? key : `${firstText(value, "eventId", "event")}:${key}`; }
|
||||
|
||||
function boundedInteger(value: unknown, min: number, max: number, fallback: number): number {
|
||||
const number = Number(value);
|
||||
return Number.isSafeInteger(number) && number >= min && number <= max ? number : fallback;
|
||||
}
|
||||
|
||||
function minimumInteger(value: unknown, min: number, fallback: number): number { const number = Number(value); return Number.isSafeInteger(number) && number >= min ? number : fallback; }
|
||||
|
||||
function normalizeGiftItems(value: unknown): Array<{ catalogCode: string; quantity: number }> {
|
||||
if (value === undefined || value === null) return [];
|
||||
if (!Array.isArray(value)) throw new Error("礼包物品格式无效。");
|
||||
@@ -435,20 +391,30 @@ function normalizeGiftItems(value: unknown): Array<{ catalogCode: string; quanti
|
||||
|
||||
function normalizeGiftOperations(value: unknown): string[] { if (value === undefined || value === null) return []; if (!Array.isArray(value)) throw new Error("礼包命令格式无效。"); return value.map((item) => { const command = isRecord(item) ? firstText(item, "command", "value") : textValue(item); if (!command.trim()) throw new Error("礼包命令不能为空。"); return command.trim(); }); }
|
||||
|
||||
function normalizeEventProduces(produces: RecordMap[]): RecordMap[] {
|
||||
return produces.map((produce) => ({
|
||||
tradeGoodsId: firstText(produce, "tradeGoodsId"),
|
||||
percent: boundedInteger(produce.percent, 0, 100, 100),
|
||||
value: minimumInteger(produce.value, 1, 1),
|
||||
r: minimumNumber(produce.r, 0, 0),
|
||||
x: finiteNumber(produce.x, 0),
|
||||
y: finiteNumber(produce.y, 0),
|
||||
z: finiteNumber(produce.z, 0)
|
||||
}));
|
||||
function normalizeRCONCommand(command: string): string {
|
||||
const normalized = command.trim();
|
||||
if (!normalized || /[\r\n]/.test(normalized)) throw new Error("SCUM RCON 命令必须是单行文本。");
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function minimumNumber(value: unknown, min: number, fallback: number): number { const number = Number(value); return Number.isFinite(number) && number >= min ? number : fallback; }
|
||||
function finiteNumber(value: unknown, fallback: number): number { const number = Number(value); return Number.isFinite(number) ? number : fallback; }
|
||||
async function dispatchSCUMRCONCommand(actions: SCUMWorkspaceActions, command: string, idempotencyKey: string): Promise<PluginBridgeExecutionResult> {
|
||||
if (!actions.dispatch) throw new Error("通用 remote.access.request 能力不可用。");
|
||||
const result = await actions.dispatch({
|
||||
requestId: idempotencyKey,
|
||||
action: "remote.access.request",
|
||||
payload: {
|
||||
capability: "remote.run.rcon.command",
|
||||
declarationKey: "scum-management",
|
||||
targetKey: "scum-management",
|
||||
idempotencyKey,
|
||||
timeoutSeconds: "30",
|
||||
maxAttempts: "1",
|
||||
"input.command": command
|
||||
}
|
||||
});
|
||||
if (result.status && !["queued", "ok"].includes(result.status)) throw new Error(result.error?.message || "SCUM RCON 命令未进入 Run 队列。");
|
||||
return result;
|
||||
}
|
||||
|
||||
function safeCommandId(value: string): string { return value.replace(/[^A-Za-z0-9_.:-]/g, "-").slice(0, 96); }
|
||||
function firstText(value: RecordMap, ...keys: string[]): string { for (const key of keys) { const result = textValue(value[key]); if (result) return result; } return ""; }
|
||||
|
||||
@@ -84,6 +84,7 @@ export function renderSCUMFeaturePage(react: ReactLike, input: SCUMPageContext)
|
||||
const [eventName, setEventName] = usePluginState(react, "");
|
||||
const [eventType, setEventType] = usePluginState(react, "range");
|
||||
const [eventSchedule, setEventSchedule] = usePluginState(react, "");
|
||||
const [eventCommand, setEventCommand] = usePluginState(react, "");
|
||||
const [eventClass, setEventClass] = usePluginState(react, "1");
|
||||
const [eventPlacard, setEventPlacard] = usePluginState(react, "");
|
||||
const [eventPercent, setEventPercent] = usePluginState(react, "100");
|
||||
@@ -152,7 +153,7 @@ export function renderSCUMFeaturePage(react: ReactLike, input: SCUMPageContext)
|
||||
state.status === "ready" ? renderSurfaceBody(e, pageKey, data, input, {
|
||||
playerSearch, setPlayerSearch, playerStatus, setPlayerStatus, playerPanel, setPlayerPanel, attributeDrafts, setAttributeDrafts, squadSearch, setSquadSearch, selectedSquadId, setSelectedSquadId,
|
||||
activityStatus, setActivityStatus, eventId, setEventId, eventName, setEventName, eventType, setEventType, eventSchedule, setEventSchedule,
|
||||
eventClass, setEventClass, eventPlacard, setEventPlacard, eventPercent, setEventPercent, eventDuration, setEventDuration, eventNpc, setEventNpc, eventItem, setEventItem, eventZombie, setEventZombie, eventAnimal, setEventAnimal,
|
||||
eventCommand, setEventCommand, eventClass, setEventClass, eventPlacard, setEventPlacard, eventPercent, setEventPercent, eventDuration, setEventDuration, eventNpc, setEventNpc, eventItem, setEventItem, eventZombie, setEventZombie, eventAnimal, setEventAnimal,
|
||||
produceEventId, setProduceEventId, produceId, setProduceId, produceTradeGoodsId, setProduceTradeGoodsId, producePercent, setProducePercent, produceValue, setProduceValue, produceRadius, setProduceRadius, produceX, setProduceX, produceY, setProduceY, produceZ, setProduceZ,
|
||||
giftTab, setGiftTab, giftCode, setGiftCode, giftName, setGiftName, giftItems, setGiftItems, giftCommands, setGiftCommands, giftClass, setGiftClass, giftAudience, setGiftAudience, giftNumber, setGiftNumber, giftAchievement, setGiftAchievement, giftAchievementNumber, setGiftAchievementNumber,
|
||||
deliveryGift, setDeliveryGift, deliveryPlayer, setDeliveryPlayer, mapSearch, setMapSearch, mapLayers, setMapLayers, selectedMapPoint, setSelectedMapPoint,
|
||||
@@ -170,6 +171,7 @@ type ViewState = {
|
||||
activityStatus: string; setActivityStatus: StateSetter<string>; giftTab: GiftTab; setGiftTab: StateSetter<GiftTab>;
|
||||
eventId: string; setEventId: StateSetter<string>; eventName: string; setEventName: StateSetter<string>;
|
||||
eventType: string; setEventType: StateSetter<string>; eventSchedule: string; setEventSchedule: StateSetter<string>;
|
||||
eventCommand: string; setEventCommand: StateSetter<string>;
|
||||
eventClass: string; setEventClass: StateSetter<string>; eventPlacard: string; setEventPlacard: StateSetter<string>; eventPercent: string; setEventPercent: StateSetter<string>;
|
||||
eventDuration: string; setEventDuration: StateSetter<string>;
|
||||
eventNpc: string; setEventNpc: StateSetter<string>; eventItem: string; setEventItem: StateSetter<string>; eventZombie: string; setEventZombie: StateSetter<string>; eventAnimal: string; setEventAnimal: StateSetter<string>;
|
||||
@@ -368,6 +370,7 @@ function activitiesSurface(e: ReactLike["createElement"], data: SCUMSurfaceData,
|
||||
if (!id || !name) throw new Error("活动编号和名称不能为空。");
|
||||
await saveEventDefinition(actions ?? {}, {
|
||||
id, name, eventType: view.eventClass === "2" ? "fixed" : "range", class: integerInput(view.eventClass, 1), schedule: view.eventSchedule.trim(), corn: view.eventSchedule.trim(),
|
||||
rconCommand: view.eventCommand.trim(), command: view.eventCommand.trim(),
|
||||
placard: view.eventPlacard.trim(), announcement: view.eventPlacard.trim(), percent: integerInput(view.eventPercent, 100), probability: integerInput(view.eventPercent, 100),
|
||||
npc: minimumIntegerInput(view.eventNpc, 0, "NPC 数量"), item: minimumIntegerInput(view.eventItem, 0, "物品数量"), zombie: minimumIntegerInput(view.eventZombie, 0, "僵尸数量"), animal: minimumIntegerInput(view.eventAnimal, 0, "动物数量"),
|
||||
status: "enabled", announce: Boolean(view.eventPlacard.trim()), durationSeconds: minimumIntegerInput(view.eventDuration, 1, "活动持续秒数"), updatedAt: new Date().toISOString()
|
||||
@@ -395,6 +398,7 @@ function activitiesSurface(e: ReactLike["createElement"], data: SCUMSurfaceData,
|
||||
labeledField(e, "活动名称", e("input", { value: view.eventName, "aria-label": "活动名称", placeholder: "例如 空投补给", onChange: (event: InputEvent) => view.setEventName(inputValue(event)) })),
|
||||
labeledField(e, "生成类型", e("select", { value: view.eventClass, "aria-label": "生成类型", onChange: (event: InputEvent) => view.setEventClass(inputValue(event)) }, e("option", { value: "1" }, "范围生成"), e("option", { value: "2" }, "固定坐标生成"))),
|
||||
labeledField(e, "活动计划", e("input", { value: view.eventSchedule, "aria-label": "活动计划", placeholder: "Cron(留空为手动)", onChange: (event: InputEvent) => view.setEventSchedule(inputValue(event)) })),
|
||||
labeledField(e, "RCON 命令", e("input", { value: view.eventCommand, "aria-label": "RCON 命令", placeholder: "例如 #start_event event_supply_drop", onChange: (event: InputEvent) => view.setEventCommand(inputValue(event)) })),
|
||||
labeledField(e, "活动公告", e("input", { value: view.eventPlacard, "aria-label": "活动公告", placeholder: "活动开始时显示的公告", onChange: (event: InputEvent) => view.setEventPlacard(inputValue(event)) })),
|
||||
labeledField(e, "触发概率 (%)", e("input", { value: view.eventPercent, "aria-label": "活动概率", type: "number", placeholder: "100", onChange: (event: InputEvent) => view.setEventPercent(inputValue(event)) })),
|
||||
labeledField(e, "持续秒数", e("input", { value: view.eventDuration, "aria-label": "活动持续秒数", type: "number", min: 1, placeholder: "1800", onChange: (event: InputEvent) => view.setEventDuration(inputValue(event)) })),
|
||||
@@ -438,8 +442,8 @@ function activitiesSurface(e: ReactLike["createElement"], data: SCUMSurfaceData,
|
||||
e("div", { className: "console-record-head" }, e("strong", null, textField(event, "name", "title") || eventId || "未命名活动"), e("span", { className: `status-pill ${activeStatus(status) ? "status-active" : "status-disabled"}` }, status)),
|
||||
e("div", { className: "console-record-meta" }, e("span", null, `生成 ${Number(field(event, "class")) === 2 ? "固定坐标" : "范围"}`), e("span", null, `计划 ${textField(event, "schedule", "corn") || "手动"}`), e("span", null, `概率 ${numField(event, "percent", "probability")}%`), e("span", null, `NPC/物品/僵尸/动物 ${numField(event, "npc")}/${numField(event, "item")}/${numField(event, "zombie")}/${numField(event, "animal")}`), e("span", null, textField(event, "placard", "announcement") || "无公告")),
|
||||
e("div", { className: "console-row-actions" },
|
||||
e("button", { type: "button", className: "primary-command", disabled: !actions?.gameClient || !actions?.pluginData, onClick: () => runAction(view.setAction, "正在启动活动…", async () => { await startEvent(actions ?? {}, event, data.eventProduces.filter((produce) => textField(produce, "eventId", "event") === eventId)); view.refresh(); return "活动命令已进入执行队列。"; }) }, "立即启动"),
|
||||
e("button", { type: "button", className: "icon-command", onClick: () => { view.setEventEditorOpen(true); view.setEventId(eventId); view.setEventName(textField(event, "name")); view.setEventClass(numField(event, "class") === "--" ? "1" : numField(event, "class")); view.setEventSchedule(textField(event, "schedule", "corn")); view.setEventPlacard(textField(event, "placard", "announcement")); view.setEventPercent(numField(event, "percent", "probability")); view.setEventDuration(numField(event, "durationSeconds") === "--" ? "1800" : numField(event, "durationSeconds")); view.setEventNpc(numField(event, "npc")); view.setEventItem(numField(event, "item")); view.setEventZombie(numField(event, "zombie")); view.setEventAnimal(numField(event, "animal")); } }, "编辑"),
|
||||
e("button", { type: "button", className: "primary-command", disabled: !actions?.dispatch || !actions?.pluginData, onClick: () => runAction(view.setAction, "正在启动活动…", async () => { await startEvent(actions ?? {}, event, data.eventProduces.filter((produce) => textField(produce, "eventId", "event") === eventId)); view.refresh(); return "活动命令已进入执行队列。"; }) }, "立即启动"),
|
||||
e("button", { type: "button", className: "icon-command", onClick: () => { view.setEventEditorOpen(true); view.setEventId(eventId); view.setEventName(textField(event, "name")); view.setEventClass(numField(event, "class") === "--" ? "1" : numField(event, "class")); view.setEventSchedule(textField(event, "schedule", "corn")); view.setEventCommand(textField(event, "rconCommand", "command")); view.setEventPlacard(textField(event, "placard", "announcement")); view.setEventPercent(numField(event, "percent", "probability")); view.setEventDuration(numField(event, "durationSeconds") === "--" ? "1800" : numField(event, "durationSeconds")); view.setEventNpc(numField(event, "npc")); view.setEventItem(numField(event, "item")); view.setEventZombie(numField(event, "zombie")); view.setEventAnimal(numField(event, "animal")); } }, "编辑"),
|
||||
e("button", { type: "button", className: "icon-command", disabled: !actions?.pluginData, onClick: () => runAction(view.setAction, "正在删除活动…", async () => { await deleteEventDefinition(actions ?? {}, eventId, data.eventProduces); view.refresh(); return "活动定义已删除。"; }) }, "删除")
|
||||
)
|
||||
);
|
||||
@@ -520,7 +524,7 @@ function giftsSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, inpu
|
||||
e("article", { className: "console-module" }, e("h2", null, "创建发放记录"),
|
||||
labeledField(e, "礼包", e("select", { value: view.deliveryGift, "aria-label": "选择礼包", onChange: (event: InputEvent) => view.setDeliveryGift(inputValue(event)) }, e("option", { value: "" }, "选择礼包"), data.gifts.map((gift, index) => { const key = textField(gift, "code", "id"); return e("option", { key: idOf(gift, `gift-option-${index}`), value: key }, textField(gift, "name") || key); }))),
|
||||
labeledField(e, "用户", e("select", { value: view.deliveryPlayer, "aria-label": "选择用户", onChange: (event: InputEvent) => view.setDeliveryPlayer(inputValue(event)) }, e("option", { value: "" }, "选择用户"), data.players.map((player, index) => { const key = textField(player, "gamePlayerId", "steamId", "id"); return e("option", { key: idOf(player, `player-option-${index}`), value: key }, textField(player, "displayName") || key); }))),
|
||||
e("button", { type: "button", className: "primary-command", disabled: !actions?.pluginData || !actions?.gameClient, onClick: queueDelivery }, "立即发放")
|
||||
e("button", { type: "button", className: "primary-command", disabled: !actions?.pluginData || !actions?.dispatch, onClick: queueDelivery }, "立即发放")
|
||||
),
|
||||
tablePanel(e, "发放记录", data.giftDeliveries, (delivery) => [textField(delivery, "playerName", "playerId") || "unknown", textField(delivery, "giftName", "giftCode") || "unknown", textField(delivery, "status") || "unknown", dateField(delivery, "deliveredAt", "createdAt")])
|
||||
) : null,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { SCUMConfigField, SCUMConfigPatch, SCUMFeatureAvailability, SCUMStateField, SCUMVehicleSpawn, SCUMVehicleSpawnOption } from "./contracts.js";
|
||||
|
||||
// These are safe fallback plugin catalogs. A Companion schema probe may narrow them
|
||||
// per server, but a game version never enables or disables a feature.
|
||||
// These are safe fallback plugin catalogs. Plugin-owned declarations may narrow
|
||||
// them per server, but a game version never enables or disables a feature.
|
||||
export const configurationCatalog: readonly SCUMConfigField[] = [
|
||||
{ key: "server-name", fileKey: "scum-server-settings", configKey: "ServerName", label: "服务器名称", description: "显示在服务器浏览器与玩家连接界面。", control: "text", defaultValue: "SCUM Server", restartImpact: "restart-required" },
|
||||
{ key: "game-port", fileKey: "scum-server-settings", configKey: "GamePort", label: "游戏端口", description: "玩家连接所使用的游戏端口。", control: "port", minimum: 1, maximum: 65535, defaultValue: "7779", restartImpact: "restart-required" },
|
||||
|
||||
Reference in New Issue
Block a user