Skip to content

Commit d7df4e9

Browse files
committed
add support to inject custom bedrock_server.exe
1 parent cd97e5e commit d7df4e9

3 files changed

Lines changed: 150 additions & 3 deletions

File tree

packages/blr/docs/reference/generated-project.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,18 @@ Optional project state file.
173173

174174
If present, `blr` applies it to BDS permissions state.
175175

176+
### `server/bedrock_server.exe`
177+
178+
Optional project state file for Windows local-server runs.
179+
180+
If present, `blr` copies it into the resolved runtime BDS server directory and uses it as the local-server executable override.
181+
182+
Notes:
183+
184+
- this overrides the provisioned `bedrock_server.exe` in the runtime BDS folder
185+
- if the runtime BDS server is already provisioned, `blr` reuses that server and applies the override without re-downloading the stock archive
186+
- `blr dev` prints a notice when the override is active
187+
176188
## Generated Support Files
177189

178190
### `.gitignore`

packages/blr/src/bds.ts

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { spawn, type ChildProcess } from "node:child_process";
22
import path from "node:path";
3-
import { mkdtemp, rename, writeFile } from "node:fs/promises";
3+
import { chmod, copyFile, mkdtemp, rename, writeFile } from "node:fs/promises";
44
import { createInterface, type Interface } from "node:readline";
55
import AdmZip from "adm-zip";
66
import { resolveDirectBedrockDownloadUrl } from "./bedrock-downloads.js";
@@ -13,6 +13,7 @@ import { resolveProjectRelativePath } from "./environment.js";
1313
import {
1414
copyDirectory,
1515
ensureDirectory,
16+
ensureParentDirectory,
1617
exists,
1718
readJson,
1819
readText,
@@ -34,7 +35,10 @@ import {
3435
resolveConfiguredWorldSourcePath,
3536
resolveProjectWorldSourceDirectory,
3637
} from "./world.js";
37-
import { resolveProjectServerStatePath } from "./server-state.js";
38+
import {
39+
PROJECT_SERVER_STATE_ROOT,
40+
resolveProjectServerStatePath,
41+
} from "./server-state.js";
3842

3943
type AllowlistEntry = {
4044
xuid: string;
@@ -63,6 +67,8 @@ export type ResolvedBdsState = {
6367
worldSourceDirectory: string;
6468
executablePath: string;
6569
zipPath: string;
70+
customExecutableSourcePath?: string;
71+
customExecutableInjected: boolean;
6672
};
6773

6874
function resolveBdsPlatform(
@@ -120,9 +126,48 @@ export function resolveBdsRuntimeState(
120126
? `bedrock-server-preview-${version}-${platform}.zip`
121127
: `bedrock-server-${version}-${platform}.zip`,
122128
),
129+
customExecutableInjected: false,
123130
};
124131
}
125132

133+
function resolveProjectCustomExecutablePath(
134+
projectRoot: string,
135+
state: ResolvedBdsState,
136+
): string {
137+
return path.join(
138+
projectRoot,
139+
PROJECT_SERVER_STATE_ROOT,
140+
path.basename(state.executablePath),
141+
);
142+
}
143+
144+
async function applyCustomExecutableOverride(
145+
projectRoot: string,
146+
state: ResolvedBdsState,
147+
debug?: DebugLogger,
148+
): Promise<void> {
149+
const customExecutableSourcePath = resolveProjectCustomExecutablePath(
150+
projectRoot,
151+
state,
152+
);
153+
if (!(await exists(customExecutableSourcePath))) {
154+
return;
155+
}
156+
157+
await ensureParentDirectory(state.executablePath);
158+
await copyFile(customExecutableSourcePath, state.executablePath);
159+
if (state.platform === "linux") {
160+
await chmod(state.executablePath, 0o755);
161+
}
162+
163+
state.customExecutableSourcePath = customExecutableSourcePath;
164+
state.customExecutableInjected = true;
165+
debug?.log("bds", "applied custom BDS executable override", {
166+
sourcePath: customExecutableSourcePath,
167+
executablePath: state.executablePath,
168+
});
169+
}
170+
126171
async function downloadIfMissing(
127172
state: ResolvedBdsState,
128173
debug?: DebugLogger,
@@ -363,6 +408,7 @@ async function extractIfMissing(
363408
await readProjectPermissions(projectRoot, config),
364409
);
365410
await ensureModulePermissions(state.serverDirectory);
411+
await applyCustomExecutableOverride(projectRoot, state, debug);
366412
return;
367413
}
368414

@@ -418,6 +464,7 @@ async function extractIfMissing(
418464
await readProjectPermissions(projectRoot, config),
419465
);
420466
await ensureModulePermissions(state.serverDirectory);
467+
await applyCustomExecutableOverride(projectRoot, state, debug);
421468
}
422469

423470
async function copyBuiltArtifacts(
@@ -889,6 +936,21 @@ export class BdsServerController {
889936
return;
890937
}
891938

939+
if (state.customExecutableInjected) {
940+
const sourceLabel = state.customExecutableSourcePath
941+
? path.relative(
942+
this.projectRoot,
943+
state.customExecutableSourcePath,
944+
)
945+
: path.join(
946+
PROJECT_SERVER_STATE_ROOT,
947+
path.basename(state.executablePath),
948+
);
949+
console.log(
950+
`[dev] Notice: using custom local-server executable override from ${sourceLabel}.`,
951+
);
952+
}
953+
892954
this.options.debug?.log("bds", "starting managed server", {
893955
executablePath: state.executablePath,
894956
cwd: state.serverDirectory,

packages/blr/test/bds.test.ts

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import assert from "node:assert/strict";
2-
import { access } from "node:fs/promises";
2+
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
33
import path from "node:path";
44
import test from "node:test";
55
import { ensureBds } from "../src/bds.js";
@@ -74,3 +74,76 @@ test("ensureBds does not leave a versioned server directory behind after an inva
7474
assert.equal(await pathExists(serverDirectory), false);
7575
assert.equal(await pathExists(cacheZipPath), false);
7676
});
77+
78+
test("ensureBds applies server/bedrock_server.exe as a custom local-server override without downloading", async (t) => {
79+
const projectRoot = await createTempDirectory(t, "blr-bds-custom-");
80+
const version = "1.26.3.1";
81+
const runtimeServerDirectory = path.join(
82+
projectRoot,
83+
".blr",
84+
"bds",
85+
version,
86+
"server",
87+
);
88+
const runtimeExecutablePath = path.join(
89+
runtimeServerDirectory,
90+
"bedrock_server.exe",
91+
);
92+
const customExecutablePath = path.join(
93+
projectRoot,
94+
"server",
95+
"bedrock_server.exe",
96+
);
97+
98+
await mkdir(path.join(runtimeServerDirectory, "config", "default"), {
99+
recursive: true,
100+
});
101+
await writeFile(runtimeExecutablePath, "stock executable");
102+
await writeFile(
103+
path.join(runtimeServerDirectory, "server.properties"),
104+
"level-name=Bedrock level\n",
105+
);
106+
await mkdir(path.dirname(customExecutablePath), { recursive: true });
107+
await writeFile(customExecutablePath, "custom executable");
108+
109+
t.mock.method(globalThis, "fetch", async () => {
110+
throw new Error("fetch should not be called");
111+
});
112+
113+
const state = await ensureBds(
114+
projectRoot,
115+
{
116+
minecraft: {
117+
channel: "stable",
118+
},
119+
dev: {
120+
localServer: {
121+
worldName: "Bedrock level",
122+
worldSourcePath: "worlds/Bedrock level",
123+
defaultPermissionLevel: "member",
124+
gamemode: "survival",
125+
allowlist: [],
126+
operators: [],
127+
},
128+
},
129+
world: {
130+
backend: "local",
131+
},
132+
} as any,
133+
{
134+
localServer: {
135+
bdsVersion: version,
136+
platform: "win",
137+
cacheDirectory: ".blr/cache/bds",
138+
serverDirectory: `.blr/bds/${version}/server`,
139+
},
140+
} as any,
141+
);
142+
143+
assert.equal(
144+
await readFile(runtimeExecutablePath, "utf8"),
145+
"custom executable",
146+
);
147+
assert.equal(state.customExecutableInjected, true);
148+
assert.equal(state.customExecutableSourcePath, customExecutablePath);
149+
});

0 commit comments

Comments
 (0)