Quaternius weapons, input fixes, sound & path fixes
- Added 6 Quaternius weapon models (Pistol, Revolver, Rifle, Shotgun, P90, SniperRifle) - Created weapon resource files with balanced stats - Wired Pistol + Rifle into player.tscn replacing Kenney blasters - Fixed WASD input (matched input map action names) - Fixed Escape key toggles mouse capture - Added Audio autoload singleton - Fixed broken sound asset paths across all scripts - Fixed all scene ext_resource text paths (nested under assets/kenney-fps/) - Fixed all .import source_file paths - Deleted stale .import files (will be regenerated by editor on open)
This commit is contained in:
Vendored
+186
@@ -0,0 +1,186 @@
|
||||
import { join } from 'path';
|
||||
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from 'fs';
|
||||
import { logDebug } from './logger.js';
|
||||
import { addAutoloadEntry, parseAutoloads, removeAutoloadEntry } from './autoload-ini.js';
|
||||
const BRIDGE_AUTOLOAD_NAME = 'McpBridge';
|
||||
const BRIDGE_SCRIPT_FILENAME = 'mcp_bridge.gd';
|
||||
const MCP_GITIGNORE_ENTRY = '.mcp/';
|
||||
// Matches the baked-port marker line inserted in src/scripts/mcp_bridge.gd —
|
||||
// `const PORT := <int>` — so inject() can rewrite the integer per project.
|
||||
const BAKED_PORT_REGEX = /const PORT := \d+/;
|
||||
/**
|
||||
* Owns the McpBridge autoload artifact: the script copy in the target project,
|
||||
* the `[autoload]` entry in project.godot, the `.mcp/.gdignore` marker, and the
|
||||
* `.gitignore` augmentation. GodotRunner delegates to this for inject/cleanup
|
||||
* during run_project / attach_project / stop_project flows.
|
||||
*
|
||||
* The project-root bridge script is runtime-owned and refreshed on first
|
||||
* injection for a manager session so a rebuilt server cannot talk to stale
|
||||
* GDScript from an earlier run. Idempotent within a session via
|
||||
* `injectedProjects`: a second `inject()` call for the same path short-circuits
|
||||
* without rewriting project.godot.
|
||||
*/
|
||||
export class BridgeManager {
|
||||
bridgeScriptPath;
|
||||
injectedProjects = new Set();
|
||||
repairedProjects = new Set();
|
||||
/**
|
||||
* Last port baked into the on-disk script per project. Used by the
|
||||
* race-detection helper in runtime-tools to spot a concurrent re-inject
|
||||
* after a bridge-wait timeout.
|
||||
*/
|
||||
lastInjectedPort = new Map();
|
||||
constructor(bridgeScriptPath) {
|
||||
this.bridgeScriptPath = bridgeScriptPath;
|
||||
}
|
||||
inject(projectPath, port) {
|
||||
// Always rewrite the destination — the per-project bridge script may
|
||||
// differ from the template by exactly the baked integer, so a size/mtime
|
||||
// shortcut no longer maps to "up-to-date." Bake the resolved port into
|
||||
// the const PORT line so the running game listens on the exact port the
|
||||
// Node side will connect to.
|
||||
const template = readFileSync(this.bridgeScriptPath, 'utf8');
|
||||
if (!BAKED_PORT_REGEX.test(template)) {
|
||||
throw new Error(`Bridge script template at ${this.bridgeScriptPath} is missing the 'const PORT := <int>' marker`);
|
||||
}
|
||||
const baked = template.replace(BAKED_PORT_REGEX, `const PORT := ${port}`);
|
||||
const destScript = join(projectPath, BRIDGE_SCRIPT_FILENAME);
|
||||
writeFileSync(destScript, baked, 'utf8');
|
||||
this.lastInjectedPort.set(projectPath, port);
|
||||
logDebug(`Wrote bridge autoload at ${destScript} (baked port ${port})`);
|
||||
if (this.injectedProjects.has(projectPath)) {
|
||||
logDebug('Bridge already injected for this project; refreshed script only.');
|
||||
return;
|
||||
}
|
||||
this.ensureMcpGdignore(projectPath);
|
||||
this.ensureGitignored(projectPath);
|
||||
const projectFile = join(projectPath, 'project.godot');
|
||||
const existing = parseAutoloads(projectFile);
|
||||
const alreadyRegistered = existing.some((a) => a.name === BRIDGE_AUTOLOAD_NAME);
|
||||
if (alreadyRegistered) {
|
||||
logDebug('Bridge autoload already present, skipping injection');
|
||||
}
|
||||
else {
|
||||
addAutoloadEntry(projectFile, BRIDGE_AUTOLOAD_NAME, BRIDGE_SCRIPT_FILENAME, true);
|
||||
logDebug('Injected bridge autoload into project.godot');
|
||||
}
|
||||
this.injectedProjects.add(projectPath);
|
||||
}
|
||||
cleanup(projectPath) {
|
||||
this.removeBridgeArtifacts(projectPath);
|
||||
this.injectedProjects.delete(projectPath);
|
||||
this.repairedProjects.delete(projectPath);
|
||||
this.lastInjectedPort.delete(projectPath);
|
||||
}
|
||||
/**
|
||||
* Read the port currently baked into the project's bridge script. Returns
|
||||
* the integer on success, or null if the file is missing, unreadable, or
|
||||
* lacks the marker. Used to detect concurrent re-inject after a bridge-
|
||||
* wait timeout (another MCP client may have rewritten the port).
|
||||
*/
|
||||
readBakedPort(projectPath) {
|
||||
const destScript = join(projectPath, BRIDGE_SCRIPT_FILENAME);
|
||||
if (!existsSync(destScript))
|
||||
return null;
|
||||
try {
|
||||
const content = readFileSync(destScript, 'utf8');
|
||||
const match = content.match(BAKED_PORT_REGEX);
|
||||
if (!match)
|
||||
return null;
|
||||
const parsed = Number.parseInt(match[0].replace(/^const PORT := /, ''), 10);
|
||||
if (!Number.isFinite(parsed) || parsed <= 0 || parsed > 65535)
|
||||
return null;
|
||||
return parsed;
|
||||
}
|
||||
catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* If project.godot still has an `McpBridge=` line but the script file is
|
||||
* missing, the autoload would crash every subsequent headless op. Detect and
|
||||
* clean the orphan before running an operation.
|
||||
*
|
||||
* Cached per project: once a path has been checked clean, skip the file
|
||||
* reads on subsequent ops in the same session.
|
||||
*/
|
||||
repairOrphaned(projectPath) {
|
||||
if (this.repairedProjects.has(projectPath))
|
||||
return;
|
||||
const projectFile = join(projectPath, 'project.godot');
|
||||
const bridgeScript = join(projectPath, BRIDGE_SCRIPT_FILENAME);
|
||||
if (!existsSync(projectFile))
|
||||
return;
|
||||
if (existsSync(bridgeScript)) {
|
||||
this.repairedProjects.add(projectPath);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const content = readFileSync(projectFile, 'utf8');
|
||||
if (content.includes(`${BRIDGE_AUTOLOAD_NAME}=`)) {
|
||||
this.removeBridgeArtifacts(projectPath);
|
||||
logDebug('Cleaned up orphaned McpBridge autoload entry');
|
||||
}
|
||||
this.repairedProjects.add(projectPath);
|
||||
}
|
||||
catch (err) {
|
||||
logDebug(`Non-fatal: Failed to check/repair orphaned bridge: ${err}`);
|
||||
}
|
||||
}
|
||||
removeBridgeArtifacts(projectPath) {
|
||||
try {
|
||||
const projectFile = join(projectPath, 'project.godot');
|
||||
if (existsSync(projectFile)) {
|
||||
const removed = removeAutoloadEntry(projectFile, BRIDGE_AUTOLOAD_NAME);
|
||||
if (removed) {
|
||||
logDebug(`Removed ${BRIDGE_AUTOLOAD_NAME} autoload from project.godot`);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
logDebug(`Non-fatal: Failed to clean ${BRIDGE_AUTOLOAD_NAME} from project.godot: ${err}`);
|
||||
}
|
||||
try {
|
||||
const scriptFile = join(projectPath, BRIDGE_SCRIPT_FILENAME);
|
||||
if (existsSync(scriptFile)) {
|
||||
unlinkSync(scriptFile);
|
||||
logDebug(`Removed ${BRIDGE_SCRIPT_FILENAME} from project`);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
logDebug(`Non-fatal: Failed to remove ${BRIDGE_SCRIPT_FILENAME}: ${err}`);
|
||||
}
|
||||
try {
|
||||
const uidFile = join(projectPath, `${BRIDGE_SCRIPT_FILENAME}.uid`);
|
||||
if (existsSync(uidFile)) {
|
||||
unlinkSync(uidFile);
|
||||
logDebug(`Removed ${BRIDGE_SCRIPT_FILENAME}.uid from project`);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
logDebug(`Non-fatal: Failed to remove ${BRIDGE_SCRIPT_FILENAME}.uid: ${err}`);
|
||||
}
|
||||
}
|
||||
ensureMcpGdignore(projectPath) {
|
||||
const mcpDir = join(projectPath, '.mcp');
|
||||
mkdirSync(mcpDir, { recursive: true });
|
||||
writeFileSync(join(mcpDir, '.gdignore'), '', 'utf8');
|
||||
logDebug('Created .mcp/.gdignore');
|
||||
}
|
||||
ensureGitignored(projectPath) {
|
||||
const gitignorePath = join(projectPath, '.gitignore');
|
||||
if (existsSync(gitignorePath)) {
|
||||
const gitignoreContent = readFileSync(gitignorePath, 'utf8');
|
||||
if (!gitignoreContent.includes(MCP_GITIGNORE_ENTRY)) {
|
||||
const newline = gitignoreContent.endsWith('\n') ? '' : '\n';
|
||||
writeFileSync(gitignorePath, gitignoreContent + newline + MCP_GITIGNORE_ENTRY + '\n', 'utf8');
|
||||
logDebug('Added .mcp/ to existing .gitignore');
|
||||
}
|
||||
}
|
||||
else {
|
||||
writeFileSync(gitignorePath, MCP_GITIGNORE_ENTRY + '\n', 'utf8');
|
||||
logDebug('Created .gitignore with .mcp/ entry');
|
||||
}
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=bridge-manager.js.map
|
||||
Reference in New Issue
Block a user