feat: add UE4SS DLL runtime extension

This commit is contained in:
npc0-hue
2026-07-23 09:49:14 +08:00
parent 8e34c8762a
commit 1caf40565c
30 changed files with 1179 additions and 70 deletions
@@ -849,6 +849,28 @@
"retainPrevious": true
}
}
],
"dllExtensions": [
{
"key": "scum-simple-rcon-ue4ss",
"displayName": "SCUM Simple RCON UE4SS DLL",
"kind": "ue4ss-dll",
"activation": "server-start",
"version": "0.1.0-unpublished",
"releaseState": "unpublished",
"releaseUrl": "https://cdn.npc0.com/scum_simple_rcon_ue4s.dll",
"targetKey": "ue4ss/scum-simple-rcon",
"modKey": "scum_simple_rcon",
"dllRef": "ue4ss/Mods/scum_simple_rcon/dlls/main.dll",
"supportedTargets": [
{
"os": "windows",
"arch": "amd64"
}
],
"updateOnStart": true,
"rconPort": 27015
}
]
}
}
@@ -124,6 +124,12 @@
"type": "array",
"items": { "$ref": "#/$defs/runtimeClientManagerProfile" },
"uniqueItems": true
},
"dllExtensions": {
"type": "array",
"items": { "$ref": "#/$defs/runtimeDLLExtensionProfile" },
"uniqueItems": true,
"maxItems": 16
}
}
},
@@ -428,6 +434,7 @@
},
"transportKeys": { "type": "array", "items": { "$ref": "#/$defs/logicalKey" }, "uniqueItems": true },
"clientManagerRef": { "$ref": "#/$defs/logicalKey" },
"dllExtensionRefs": { "type": "array", "items": { "$ref": "#/$defs/logicalKey" }, "uniqueItems": true, "maxItems": 16 },
"platforms": { "type": "array", "items": { "$ref": "#/$defs/runtimePlatform" }, "uniqueItems": true }
}
},
@@ -668,6 +675,36 @@
}
]
},
"runtimeDLLExtensionProfile": {
"type": "object",
"required": ["key", "displayName", "kind", "activation", "version", "releaseState", "targetKey", "modKey", "dllRef", "supportedTargets", "updateOnStart", "rconPort"],
"additionalProperties": false,
"properties": {
"key": { "$ref": "#/$defs/logicalKey" },
"displayName": { "type": "string", "minLength": 1, "maxLength": 80 },
"kind": { "const": "ue4ss-dll" },
"activation": { "const": "server-start" },
"version": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(?:-[0-9A-Za-z.-]+)?$", "maxLength": 40 },
"releaseState": { "enum": ["unpublished", "ready"] },
"releaseUrl": { "type": "string", "pattern": "^https://[a-zA-Z0-9._~:/\\[\\]@!$&'()*+,;=%-]+\\.dll$", "maxLength": 240 },
"checksum": { "type": "string", "pattern": "^sha256:[a-fA-F0-9]{64}$" },
"sizeBytes": { "type": "integer", "minimum": 1, "maximum": 134217728 },
"targetKey": { "$ref": "#/$defs/logicalKey" },
"modKey": { "type": "string", "pattern": "^[a-z0-9][a-z0-9_-]{0,79}$" },
"dllRef": { "type": "string", "pattern": "^ue4ss/Mods/[a-z0-9][a-z0-9_-]{0,79}/dlls/main\\.dll$", "maxLength": 160 },
"scumExecutableChecksum": { "type": "string", "pattern": "^sha256:[a-fA-F0-9]{64}$" },
"ue4ssAbi": { "type": "string", "pattern": "^[A-Za-z0-9._-]{1,80}$" },
"supportedTargets": { "type": "array", "items": { "$ref": "#/$defs/runtimeTarget" }, "minItems": 1, "maxItems": 1, "uniqueItems": true },
"updateOnStart": { "const": true },
"rconPort": { "type": "integer", "minimum": 1024, "maximum": 65535 }
},
"allOf": [
{
"if": { "properties": { "releaseState": { "const": "ready" } }, "required": ["releaseState"] },
"then": { "required": ["releaseUrl", "checksum", "sizeBytes", "scumExecutableChecksum", "ue4ssAbi"] }
}
]
},
"aiPurpose": {
"enum": [
"config.read",
+95
View File
@@ -461,6 +461,100 @@ function validateClientManagerProfiles(manifest: unknown): string[] {
return errors;
}
function validateDLLExtensionProfiles(manifest: unknown): string[] {
if (typeof manifest !== "object" || manifest === null) {
return [];
}
type DLLExtensionProfile = {
key?: string;
kind?: string;
activation?: string;
releaseState?: string;
releaseUrl?: string;
checksum?: string;
sizeBytes?: number;
targetKey?: string;
modKey?: string;
dllRef?: string;
scumExecutableChecksum?: string;
ue4ssAbi?: string;
supportedTargets?: Array<{ os?: string; arch?: string }>;
updateOnStart?: boolean;
rconPort?: number;
};
type LifecycleProfile = { key?: string; mode?: string; capabilities?: string[]; dllExtensionRefs?: string[]; platforms?: string[] };
const runtimeProfiles = (manifest as { runtimeProfiles?: { dllExtensions?: DLLExtensionProfile[]; lifecycleProfiles?: LifecycleProfile[] } }).runtimeProfiles;
const profiles = runtimeProfiles?.dllExtensions ?? [];
const errors: string[] = [];
const checksumPattern = /^sha256:[a-fA-F0-9]{64}$/;
const privateIPv4 = /^(127\.|10\.|192\.168\.|169\.254\.|172\.(1[6-9]|2\d|3[01])\.)/;
for (const [index, profile] of profiles.entries()) {
const location = `manifest.runtimeProfiles.dllExtensions[${index}]`;
if (profile.kind !== "ue4ss-dll" || profile.activation !== "server-start") {
errors.push(`${location}: only a server-start ue4ss-dll extension is allowed`);
}
if (profile.dllRef !== `ue4ss/Mods/${profile.modKey ?? ""}/dlls/main.dll`) {
errors.push(`${location}.dllRef: must be the declared UE4SS mod main.dll path`);
}
if (profile.updateOnStart !== true) {
errors.push(`${location}.updateOnStart: must be true for a managed DLL extension`);
}
if (!Number.isInteger(profile.rconPort) || (profile.rconPort ?? 0) < 1024 || (profile.rconPort ?? 0) > 65535) {
errors.push(`${location}.rconPort: must be an unprivileged TCP port`);
}
const target = profile.supportedTargets?.[0];
if (profile.supportedTargets?.length !== 1 || target?.os !== "windows" || target?.arch !== "amd64") {
errors.push(`${location}.supportedTargets: UE4SS DLL extensions support only windows/amd64`);
}
if (profile.releaseState !== "ready" && profile.releaseState !== "unpublished") {
errors.push(`${location}.releaseState: must be ready or unpublished`);
}
if (profile.releaseState === "ready") {
if (!checksumPattern.test(profile.checksum ?? "") || !checksumPattern.test(profile.scumExecutableChecksum ?? "")) {
errors.push(`${location}: release and SCUM executable SHA-256 checksums are required`);
}
if (!Number.isInteger(profile.sizeBytes) || (profile.sizeBytes ?? 0) < 1 || (profile.sizeBytes ?? 0) > 128 * 1024 * 1024) {
errors.push(`${location}.sizeBytes: must be a bounded DLL size`);
}
if (!profile.ue4ssAbi) {
errors.push(`${location}.ue4ssAbi: an ABI marker is required`);
}
}
if (profile.releaseUrl) {
try {
const parsed = new URL(profile.releaseUrl);
const host = parsed.hostname.toLowerCase();
if (parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.search || parsed.hash || host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || host === "::1" || privateIPv4.test(host) || !parsed.pathname.toLowerCase().endsWith(".dll")) {
errors.push(`${location}.releaseUrl: must be an approved public HTTPS DLL release`);
}
} catch {
errors.push(`${location}.releaseUrl: URL is invalid`);
}
} else if (profile.releaseState === "ready") {
errors.push(`${location}.releaseUrl: a ready release URL is required`);
}
}
const declared = new Set(profiles.map((profile) => profile.key).filter((key): key is string => Boolean(key)));
const stateByKey = new Map(profiles.map((profile) => [profile.key, profile.releaseState]));
for (const [index, profile] of (runtimeProfiles?.lifecycleProfiles ?? []).entries()) {
const extensionRefs = profile.dllExtensionRefs ?? [];
if (extensionRefs.length > 0 && (profile.mode !== "local-process" || !profile.capabilities?.includes("process.start") || profile.platforms?.length !== 1 || profile.platforms[0] !== "windows")) {
errors.push(`manifest.runtimeProfiles.lifecycleProfiles[${index}]: a DLL extension requires a windows local-process profile with process.start`);
}
for (const key of extensionRefs) {
if (!declared.has(key)) {
errors.push(`manifest.runtimeProfiles.lifecycleProfiles[${index}].dllExtensionRefs: undeclared DLL extension ${key}`);
}
if (stateByKey.get(key) !== "ready") {
errors.push(`manifest.runtimeProfiles.lifecycleProfiles[${index}].dllExtensionRefs: DLL extension ${key} is not ready for activation`);
}
}
}
return errors;
}
export function validateGameClientBridgeCatalog(manifest: unknown): string[] {
if (typeof manifest !== "object" || manifest === null) {
return [];
@@ -1011,6 +1105,7 @@ export function validateManifestFile(manifestPath: string): string[] {
errors.push(...scanUnsafeValues(manifest, "manifest"));
errors.push(...validateDependencyPlans(manifest));
errors.push(...validateClientManagerProfiles(manifest));
errors.push(...validateDLLExtensionProfiles(manifest));
errors.push(...validateGameClientBridgeCatalog(manifest));
errors.push(...validateGameClientBridgeSchemaFiles(manifest, manifestDir));
errors.push(...validateGameClientBridgeCompanionConfig(manifest, manifestDir));
+22
View File
@@ -388,6 +388,7 @@ export interface RuntimeLifecycleProfile {
actionRefs?: Partial<Record<PluginLifecycleAction, string>>;
transportKeys?: string[];
clientManagerRef?: string;
dllExtensionRefs?: string[];
platforms?: RuntimePlatform[];
}
@@ -497,6 +498,26 @@ export interface RuntimeClientManagerProfile {
};
}
export interface RuntimeDLLExtensionProfile {
key: string;
displayName: string;
kind: "ue4ss-dll";
activation: "server-start";
version: string;
releaseState: "unpublished" | "ready";
releaseUrl?: string;
checksum?: `sha256:${string}`;
sizeBytes?: number;
targetKey: string;
modKey: string;
dllRef: string;
scumExecutableChecksum?: `sha256:${string}`;
ue4ssAbi?: string;
supportedTargets: [{ os: "windows"; arch: "amd64" }];
updateOnStart: true;
rconPort: number;
}
export interface GamePluginRuntimeProfiles {
discovery?: RuntimeDiscoveryProbe[];
lifecycleProfiles?: RuntimeLifecycleProfile[];
@@ -506,6 +527,7 @@ export interface GamePluginRuntimeProfiles {
logEvents?: RuntimeLogEventDeclaration[];
transportProfiles?: RuntimeTransportProfile[];
clientManagers?: RuntimeClientManagerProfile[];
dllExtensions?: RuntimeDLLExtensionProfile[];
}
export interface PluginArtifactReference {
+47
View File
@@ -173,6 +173,53 @@ describe("plugin manifest validation", () => {
expect(validateManifestFile("examples/scum-server-plugin/manifest.json")).toEqual([]);
});
it("accepts a pinned ready SCUM UE4SS DLL release and lifecycle reference", () => {
const errors = validateTemporaryScumCompanionManifest((manifest) => {
const extension = manifest.runtimeProfiles.dllExtensions[0];
extension.releaseState = "ready";
extension.version = "0.1.0";
extension.checksum = `sha256:${"a".repeat(64)}`;
extension.sizeBytes = 1048576;
extension.scumExecutableChecksum = `sha256:${"b".repeat(64)}`;
extension.ue4ssAbi = "ue4ss-3.0";
manifest.runtimeProfiles.lifecycleProfiles[0].dllExtensionRefs = [extension.key];
});
expect(errors).toEqual([]);
});
it("rejects unpinned, unsafe, or unpublished SCUM UE4SS DLL activation", () => {
const missingPins = validateTemporaryScumCompanionManifest((manifest) => {
const extension = manifest.runtimeProfiles.dllExtensions[0];
extension.releaseState = "ready";
manifest.runtimeProfiles.lifecycleProfiles[0].dllExtensionRefs = [extension.key];
});
expect(missingPins.some((error) => error.includes("checksum") || error.includes("sizeBytes") || error.includes("ue4ssAbi"))).toBe(true);
const unsafeURL = validateTemporaryScumCompanionManifest((manifest) => {
const extension = manifest.runtimeProfiles.dllExtensions[0];
extension.releaseState = "ready";
extension.releaseUrl = "https://127.0.0.1/scum.exe";
extension.checksum = `sha256:${"a".repeat(64)}`;
extension.sizeBytes = 1048576;
extension.scumExecutableChecksum = `sha256:${"b".repeat(64)}`;
extension.ue4ssAbi = "ue4ss-3.0";
});
expect(unsafeURL.some((error) => error.includes("releaseUrl"))).toBe(true);
const queryURL = validateTemporaryScumCompanionManifest((manifest) => {
manifest.runtimeProfiles.dllExtensions[0].releaseUrl = "https://cdn.npc0.com/scum_simple_rcon_ue4s.dll?release=.dll";
});
expect(queryURL.some((error) => error.includes("releaseUrl"))).toBe(true);
const unpublishedReference = validateTemporaryScumCompanionManifest((manifest) => {
const extension = manifest.runtimeProfiles.dllExtensions[0];
manifest.runtimeProfiles.lifecycleProfiles[0].dllExtensionRefs = [extension.key];
manifest.runtimeProfiles.lifecycleProfiles[0].platforms = ["windows", "linux"];
});
expect(unpublishedReference.some((error) => error.includes("not ready for activation"))).toBe(true);
expect(unpublishedReference.some((error) => error.includes("windows local-process"))).toBe(true);
});
it("defines a generated SCUM companion config without inline proof or session material", () => {
const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin");
const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as {