chore: initial commit of foundryvtt-admin
Spun out of the hbm-books working folder into its own repo. A small backend+frontend+COBOL admin tool for managing Foundry VTT worlds (socket inspection, join/shutdown debugging).
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
FROM oven/bun:latest
|
||||
WORKDIR /app
|
||||
COPY package.json tsconfig.json ./
|
||||
RUN bun install
|
||||
COPY src ./src
|
||||
EXPOSE 3000
|
||||
CMD ["bun", "run", "src/index.ts"]
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "foundryvtt-admin-backend",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "bun run src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"socket.io-client": "^4.7.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { io } from "socket.io-client";
|
||||
import { fetch } from "bun";
|
||||
|
||||
const FOUNDRY_URL = process.env.FOUNDRY_URL || "http://192.168.0.3:30000/";
|
||||
|
||||
function getFoundryUrl(subpath: string): string {
|
||||
const base = FOUNDRY_URL.endsWith("/") ? FOUNDRY_URL.slice(0, -1) : FOUNDRY_URL;
|
||||
const normalizedPath = subpath.startsWith("/") ? subpath : `/${subpath}`;
|
||||
return `${base}${normalizedPath}`;
|
||||
}
|
||||
|
||||
async function run() {
|
||||
console.log("Fetching join page to get guest cookie...");
|
||||
const joinPageRes = await fetch(getFoundryUrl("/join"));
|
||||
const cookies = joinPageRes.headers.getSetCookie();
|
||||
let guestCookie = "";
|
||||
for (const c of cookies) {
|
||||
if (c.startsWith("session=")) {
|
||||
guestCookie = c.split(";")[0];
|
||||
break;
|
||||
}
|
||||
}
|
||||
console.log("Guest cookie:", guestCookie);
|
||||
|
||||
console.log("Connecting via Socket.io to:", getFoundryUrl("/"));
|
||||
const socket = io(getFoundryUrl("/"), {
|
||||
extraHeaders: {
|
||||
Cookie: guestCookie
|
||||
},
|
||||
timeout: 5000,
|
||||
transports: ["websocket", "polling"] // try both
|
||||
});
|
||||
|
||||
socket.on("connect", () => {
|
||||
console.log("Socket connected!");
|
||||
console.log("Emitting getJoinData...");
|
||||
socket.emit("getJoinData", (data: any) => {
|
||||
console.log("Callback received!");
|
||||
console.log("Data keys:", data ? Object.keys(data) : "null");
|
||||
console.log("World:", data ? JSON.stringify(data.world, null, 2) : "null");
|
||||
socket.disconnect();
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("connect_error", (err) => {
|
||||
console.log("connect_error:", err.message, err);
|
||||
});
|
||||
|
||||
socket.on("disconnect", (reason) => {
|
||||
console.log("disconnect:", reason);
|
||||
});
|
||||
|
||||
socket.on("error", (err) => {
|
||||
console.log("error event:", err);
|
||||
});
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -0,0 +1,41 @@
|
||||
import { io } from "socket.io-client";
|
||||
|
||||
const FOUNDRY_URL = process.env.FOUNDRY_URL;
|
||||
|
||||
async function run() {
|
||||
console.log(`Connecting via Socket.io to: ${FOUNDRY_URL}...`);
|
||||
try {
|
||||
const socket = io(FOUNDRY_URL);
|
||||
|
||||
socket.on("connect", () => {
|
||||
console.log("Socket connected! Emitting getJoinData...");
|
||||
socket.emit("getJoinData", (data: any) => {
|
||||
console.log("Received getJoinData response!");
|
||||
console.log("Data keys:", Object.keys(data));
|
||||
|
||||
if (data.users) {
|
||||
console.log("Users count:", data.users.length);
|
||||
console.log("Users list:", data.users.map((u: any) => ({
|
||||
id: u.id,
|
||||
name: u.name,
|
||||
role: u.role,
|
||||
active: u.active
|
||||
})));
|
||||
} else {
|
||||
console.log("No users in join data:", data);
|
||||
}
|
||||
|
||||
socket.disconnect();
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("connect_error", (err) => {
|
||||
console.error("Connection error:", err);
|
||||
});
|
||||
|
||||
} catch (err: any) {
|
||||
console.error("Error:", err);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -0,0 +1,64 @@
|
||||
import { fetch } from "bun";
|
||||
|
||||
const FOUNDRY_URL = process.env.FOUNDRY_URL;
|
||||
const FOUNDRY_ADMIN_KEY = process.env.FOUNDRY_ADMIN_KEY;
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
// 1. Authenticate with admin password to get session cookie
|
||||
const payload = new URLSearchParams();
|
||||
payload.append("action", "adminPassword");
|
||||
payload.append("adminPassword", FOUNDRY_ADMIN_KEY);
|
||||
|
||||
console.log("Authenticating...");
|
||||
const res = await fetch(`${FOUNDRY_URL}/setup`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: payload.toString(),
|
||||
redirect: "manual",
|
||||
});
|
||||
|
||||
console.log(`Auth Status: ${res.status}`);
|
||||
const cookies = res.headers.getSetCookie();
|
||||
console.log("Cookies:", cookies);
|
||||
|
||||
let sessionCookie = "";
|
||||
for (const cookie of cookies) {
|
||||
if (cookie.startsWith("session=")) {
|
||||
sessionCookie = cookie.split(";")[0];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!sessionCookie) {
|
||||
console.log("No session cookie returned.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Post shutdown request
|
||||
console.log("Sending shutdown request...");
|
||||
const shutdownRes = await fetch(`${FOUNDRY_URL}/setup`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Cookie: sessionCookie,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
shutdown: true
|
||||
}),
|
||||
redirect: "manual"
|
||||
});
|
||||
|
||||
console.log(`Shutdown Res Status: ${shutdownRes.status}`);
|
||||
const text = await shutdownRes.text();
|
||||
console.log(`Response length: ${text.length}`);
|
||||
console.log(`Response headers:`, shutdownRes.headers);
|
||||
|
||||
} catch (err: any) {
|
||||
console.error("Error:", err);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -0,0 +1,127 @@
|
||||
import { fetch } from "bun";
|
||||
import { io } from "socket.io-client";
|
||||
|
||||
const FOUNDRY_URL = process.env.FOUNDRY_URL || "http://192.168.0.3:30000/";
|
||||
const FOUNDRY_USER = "Gamemaster";
|
||||
const FOUNDRY_PASSWORD = "Gamemaster123!";
|
||||
const FOUNDRY_WORLD = "a-story-yet-to-be-finished";
|
||||
|
||||
function getFoundryUrl(subpath: string): string {
|
||||
const base = FOUNDRY_URL.endsWith("/") ? FOUNDRY_URL.slice(0, -1) : FOUNDRY_URL;
|
||||
const normalizedPath = subpath.startsWith("/") ? subpath : `/${subpath}`;
|
||||
return `${base}${normalizedPath}`;
|
||||
}
|
||||
|
||||
async function fetchUserId(cookie: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tempSocket = io(getFoundryUrl("/"), {
|
||||
forceNew: true,
|
||||
multiplex: false,
|
||||
transports: ["websocket", "polling"],
|
||||
extraHeaders: { Cookie: cookie },
|
||||
timeout: 5000
|
||||
});
|
||||
tempSocket.on("connect", () => {
|
||||
tempSocket.emit("getJoinData", (data: any) => {
|
||||
tempSocket.disconnect();
|
||||
if (data && data.users) {
|
||||
const user = data.users.find((u: any) => u.name.toLowerCase() === FOUNDRY_USER.toLowerCase());
|
||||
if (user) resolve(user._id || user.id);
|
||||
else reject(new Error("User not found"));
|
||||
} else {
|
||||
reject(new Error("No users list"));
|
||||
}
|
||||
});
|
||||
});
|
||||
tempSocket.on("connect_error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
console.log("Fetching guest cookie...");
|
||||
const joinRes = await fetch(getFoundryUrl("/join"));
|
||||
const cookies = joinRes.headers.getSetCookie();
|
||||
let guestCookie = "";
|
||||
for (const c of cookies) {
|
||||
if (c.startsWith("session=")) {
|
||||
guestCookie = c.split(";")[0];
|
||||
break;
|
||||
}
|
||||
}
|
||||
console.log("Guest Cookie:", guestCookie);
|
||||
const userId = await fetchUserId(guestCookie);
|
||||
console.log("User ID:", userId);
|
||||
|
||||
const loginPayload = new URLSearchParams();
|
||||
loginPayload.append("userid", userId);
|
||||
loginPayload.append("password", FOUNDRY_PASSWORD);
|
||||
loginPayload.append("world", FOUNDRY_WORLD);
|
||||
loginPayload.append("step", "join");
|
||||
|
||||
const loginRes = await fetch(getFoundryUrl("/join"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Cookie: guestCookie
|
||||
},
|
||||
body: loginPayload.toString(),
|
||||
redirect: "manual"
|
||||
});
|
||||
|
||||
const setCookieHeaders = loginRes.headers.getSetCookie();
|
||||
let sessionCookie = "";
|
||||
for (const cookie of setCookieHeaders) {
|
||||
if (cookie.startsWith("session=")) {
|
||||
sessionCookie = cookie.split(";")[0];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!sessionCookie && loginRes.status === 200) {
|
||||
sessionCookie = guestCookie;
|
||||
}
|
||||
console.log("Session Cookie:", sessionCookie);
|
||||
|
||||
console.log("Connecting websocket...");
|
||||
const socket = io(getFoundryUrl("/"), {
|
||||
forceNew: true,
|
||||
multiplex: false,
|
||||
transports: ["websocket", "polling"],
|
||||
extraHeaders: { Cookie: sessionCookie }
|
||||
});
|
||||
|
||||
socket.on("connect", () => {
|
||||
console.log("Connected! Emitting getDocuments...");
|
||||
|
||||
// Test 1: legacy getDocuments event
|
||||
socket.emit("getDocuments", "Actor", { query: {} }, (res: any) => {
|
||||
console.log("getDocuments response:", res ? Object.keys(res) : "null");
|
||||
});
|
||||
|
||||
// Test 2: new modifyDocument get event
|
||||
const req = {
|
||||
type: "Actor",
|
||||
action: "get",
|
||||
operation: { query: {} }
|
||||
};
|
||||
socket.emit("modifyDocument", req, (res: any) => {
|
||||
console.log("modifyDocument response keys:", res ? Object.keys(res) : "null");
|
||||
if (res && res.result) {
|
||||
console.log(`Fetched ${res.result.length} actors! First actor:`, JSON.stringify(res.result[0]));
|
||||
} else {
|
||||
console.log("Response did not contain result array. Full response:", JSON.stringify(res));
|
||||
}
|
||||
socket.disconnect();
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("connect_error", (err) => {
|
||||
console.log("Connect error:", err);
|
||||
});
|
||||
|
||||
} catch (e: any) {
|
||||
console.log("Error:", e.message);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -0,0 +1,75 @@
|
||||
import { fetch } from "bun";
|
||||
import { io } from "socket.io-client";
|
||||
|
||||
const FOUNDRY_URL = process.env.FOUNDRY_URL || "http://192.168.0.3:30000/";
|
||||
const FOUNDRY_ADMIN_KEY = process.env.FOUNDRY_ADMIN_KEY || "3ldrakarXH0mebrew";
|
||||
|
||||
function getFoundryUrl(subpath: string): string {
|
||||
const base = FOUNDRY_URL.endsWith("/") ? FOUNDRY_URL.slice(0, -1) : FOUNDRY_URL;
|
||||
const normalizedPath = subpath.startsWith("/") ? subpath : `/${subpath}`;
|
||||
return `${base}${normalizedPath}`;
|
||||
}
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
console.log("Authenticating...");
|
||||
const res = await fetch(getFoundryUrl("/setup"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: "adminPassword",
|
||||
adminPassword: FOUNDRY_ADMIN_KEY
|
||||
}),
|
||||
redirect: "manual",
|
||||
});
|
||||
|
||||
console.log("Auth status:", res.status);
|
||||
const cookies = res.headers.getSetCookie();
|
||||
console.log("Cookies:", cookies);
|
||||
|
||||
let sessionCookie = "";
|
||||
for (const cookie of cookies) {
|
||||
if (cookie.startsWith("session=")) {
|
||||
sessionCookie = cookie.split(";")[0];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!sessionCookie) {
|
||||
console.log("No session cookie!");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Connecting to setup socket...");
|
||||
const socketSetup = io(getFoundryUrl("/"), {
|
||||
extraHeaders: {
|
||||
Cookie: sessionCookie
|
||||
},
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
socketSetup.on("connect", () => {
|
||||
console.log("Connected to setup socket! Emitting getSetupData...");
|
||||
socketSetup.emit("getSetupData", (data: any) => {
|
||||
console.log("Received data. Keys:", data ? Object.keys(data) : "null/undefined");
|
||||
if (data) {
|
||||
console.log("isAdmin:", data.isAdmin);
|
||||
console.log("worlds:", data.worlds ? data.worlds.length : "undefined");
|
||||
}
|
||||
socketSetup.disconnect();
|
||||
});
|
||||
});
|
||||
|
||||
socketSetup.on("connect_error", (err) => {
|
||||
console.log("Connect error:", err.message, err);
|
||||
socketSetup.disconnect();
|
||||
});
|
||||
|
||||
} catch (e: any) {
|
||||
console.log("Error:", e.message);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -0,0 +1,29 @@
|
||||
import { fetch } from "bun";
|
||||
import * as fs from "fs";
|
||||
|
||||
const FOUNDRY_URL = process.env.FOUNDRY_URL;
|
||||
|
||||
async function run() {
|
||||
console.log(`Fetching join page from: ${FOUNDRY_URL}/join`);
|
||||
try {
|
||||
const res = await fetch(`${FOUNDRY_URL}/join`);
|
||||
console.log(`Status: ${res.status}`);
|
||||
console.log(`Redirected URL: ${res.url}`);
|
||||
|
||||
const html = await res.text();
|
||||
console.log(`HTML Length: ${html.length}`);
|
||||
fs.writeFileSync("src/join-output.html", html, "utf8");
|
||||
console.log("Saved HTML to src/join-output.html");
|
||||
|
||||
// Check if there are any options
|
||||
const optionMatches = html.match(/<option[^>]*>([\s\S]*?)<\/option>/g);
|
||||
console.log("Found options count:", optionMatches ? optionMatches.length : 0);
|
||||
if (optionMatches) {
|
||||
console.log("Options:", optionMatches.slice(0, 10));
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("Error:", err);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -0,0 +1,940 @@
|
||||
import { io } from "socket.io-client";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const FOUNDRY_URL = process.env.FOUNDRY_URL || "http://localhost:30000";
|
||||
|
||||
function getFoundryUrl(subpath: string): string {
|
||||
const base = FOUNDRY_URL.endsWith("/") ? FOUNDRY_URL.slice(0, -1) : FOUNDRY_URL;
|
||||
const normalizedPath = subpath.startsWith("/") ? subpath : `/${subpath}`;
|
||||
return `${base}${normalizedPath}`;
|
||||
}
|
||||
|
||||
const SHARED_DIR = "/app/shared";
|
||||
const ACTORS_FILE = path.join(SHARED_DIR, "ACTORS.DAT");
|
||||
const UPDATES_FILE = path.join(SHARED_DIR, "UPDATES.DAT");
|
||||
const WORLDS_CACHE_FILE = path.join(SHARED_DIR, "WORLDS_CACHE.json");
|
||||
|
||||
const FOUNDRY_ADMIN_KEY = process.env.FOUNDRY_ADMIN_KEY || "";
|
||||
let currentWorld = process.env.FOUNDRY_WORLD || "";
|
||||
let currentWorldTitle = "";
|
||||
let currentSystem = "";
|
||||
let currentUser = process.env.FOUNDRY_USER || "Admin";
|
||||
let currentPassword = process.env.FOUNDRY_PASSWORD || "";
|
||||
|
||||
let socket: any = null;
|
||||
let actorsList: any[] = [];
|
||||
|
||||
interface SystemMapping {
|
||||
getHP: (actor: any) => { value: number; max: number };
|
||||
setHP: (hp: { value: number; max: number }, actorSystem: any) => void;
|
||||
getLevel: (actor: any) => number;
|
||||
setLevel: (level: number, actorSystem: any) => void;
|
||||
}
|
||||
|
||||
const SYSTEM_MAPPINGS: Record<string, SystemMapping> = {
|
||||
"hbm-rpg-v3": {
|
||||
getHP: (actor) => {
|
||||
const h = actor.system?.attributes?.health || actor.system?.attributes?.hp || {};
|
||||
return { value: h.value ?? 0, max: h.max ?? 0 };
|
||||
},
|
||||
setHP: (hp, actorSystem) => {
|
||||
actorSystem.attributes = actorSystem.attributes || {};
|
||||
actorSystem.attributes.health = { value: hp.value, max: hp.max };
|
||||
actorSystem.attributes.hp = { value: hp.value, max: hp.max };
|
||||
},
|
||||
getLevel: (actor) => {
|
||||
return actor.system?.details?.year ?? actor.system?.details?.level ?? 0;
|
||||
},
|
||||
setLevel: (level, actorSystem) => {
|
||||
actorSystem.details = actorSystem.details || {};
|
||||
actorSystem.details.year = level;
|
||||
actorSystem.details.level = level;
|
||||
}
|
||||
},
|
||||
"dnd5e": {
|
||||
getHP: (actor) => {
|
||||
const hp = actor.system?.attributes?.hp || {};
|
||||
return { value: hp.value ?? 0, max: hp.max ?? 0 };
|
||||
},
|
||||
setHP: (hp, actorSystem) => {
|
||||
actorSystem.attributes = actorSystem.attributes || {};
|
||||
actorSystem.attributes.hp = { value: hp.value, max: hp.max };
|
||||
},
|
||||
getLevel: (actor) => {
|
||||
return actor.system?.details?.level ?? 0;
|
||||
},
|
||||
setLevel: (level, actorSystem) => {
|
||||
actorSystem.details = actorSystem.details || {};
|
||||
actorSystem.details.level = level;
|
||||
}
|
||||
},
|
||||
"pf2e": {
|
||||
getHP: (actor) => {
|
||||
const hp = actor.system?.attributes?.hp || {};
|
||||
return { value: hp.value ?? 0, max: hp.max ?? 0 };
|
||||
},
|
||||
setHP: (hp, actorSystem) => {
|
||||
actorSystem.attributes = actorSystem.attributes || {};
|
||||
actorSystem.attributes.hp = { value: hp.value, max: hp.max };
|
||||
},
|
||||
getLevel: (actor) => {
|
||||
return actor.system?.details?.level?.value ?? actor.system?.details?.level ?? 0;
|
||||
},
|
||||
setLevel: (level, actorSystem) => {
|
||||
actorSystem.details = actorSystem.details || {};
|
||||
actorSystem.details.level = actorSystem.details.level || {};
|
||||
if (typeof actorSystem.details.level === 'object') {
|
||||
actorSystem.details.level.value = level;
|
||||
} else {
|
||||
actorSystem.details.level = level;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function getActiveMapping(): SystemMapping {
|
||||
return SYSTEM_MAPPINGS[currentSystem] || SYSTEM_MAPPINGS["dnd5e"];
|
||||
}
|
||||
|
||||
if (!fs.existsSync(SHARED_DIR)) {
|
||||
fs.mkdirSync(SHARED_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
function disconnectFromFoundry() {
|
||||
if (socket) {
|
||||
console.log("Disconnecting existing WebSocket from Foundry VTT...");
|
||||
socket.disconnect();
|
||||
socket = null;
|
||||
}
|
||||
actorsList = [];
|
||||
writeActorsToDat();
|
||||
}
|
||||
|
||||
async function fetchUserId(cookie: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tempSocket = io(getFoundryUrl("/"), {
|
||||
forceNew: true,
|
||||
multiplex: false,
|
||||
transports: ["websocket", "polling"],
|
||||
extraHeaders: {
|
||||
Cookie: cookie
|
||||
},
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
tempSocket.on("connect", () => {
|
||||
tempSocket.emit("getJoinData", (data: any) => {
|
||||
tempSocket.disconnect();
|
||||
console.log("getJoinData response:", JSON.stringify(data));
|
||||
if (data && data.users) {
|
||||
if (data.world) {
|
||||
currentWorldTitle = data.world.title || "";
|
||||
currentSystem = data.world.system || "";
|
||||
}
|
||||
const user = data.users.find((u: any) => u.name.toLowerCase() === currentUser.toLowerCase());
|
||||
if (user) {
|
||||
resolve(user._id || user.id);
|
||||
} else {
|
||||
reject(new Error(`User "${currentUser}" not found in world users list. Available: ${data.users.map((u: any) => u.name).join(", ")}`));
|
||||
}
|
||||
} else {
|
||||
reject(new Error(`Failed to retrieve users list. Data: ${data ? JSON.stringify(data) : "null"}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
tempSocket.on("connect_error", (err) => {
|
||||
tempSocket.disconnect();
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function connectToFoundry() {
|
||||
disconnectFromFoundry();
|
||||
if (!currentWorld) {
|
||||
console.log("No active world configured. Waiting for world selection... ");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Connecting to Foundry VTT world "${currentWorld}" as "${currentUser}" at ${FOUNDRY_URL}...`);
|
||||
try {
|
||||
const joinPageRes = await fetch(getFoundryUrl("/join"));
|
||||
if (joinPageRes.url.includes("/setup")) {
|
||||
console.log(`World "${currentWorld}" is not running. Please launch it first.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const cookies = joinPageRes.headers.getSetCookie();
|
||||
console.log("Response cookies from join page:", cookies);
|
||||
let guestCookie = "";
|
||||
for (const c of cookies) {
|
||||
if (c.startsWith("session=")) {
|
||||
guestCookie = c.split(";")[0];
|
||||
break;
|
||||
}
|
||||
}
|
||||
console.log("Extracted guest cookie:", guestCookie);
|
||||
|
||||
if (!guestCookie) {
|
||||
throw new Error("Failed to obtain guest session cookie from join page.");
|
||||
}
|
||||
|
||||
console.log("Fetching User ID from join socket...");
|
||||
const userId = await fetchUserId(guestCookie);
|
||||
console.log(`Found User ID for "${currentUser}": ${userId}`);
|
||||
|
||||
const postData = {
|
||||
userid: userId,
|
||||
password: currentPassword,
|
||||
action: "join"
|
||||
};
|
||||
|
||||
console.log(`Sending POST /join with JSON payload...`);
|
||||
const loginRes = await fetch(getFoundryUrl("/join"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Cookie: guestCookie
|
||||
},
|
||||
body: JSON.stringify(postData),
|
||||
redirect: "manual"
|
||||
});
|
||||
|
||||
console.log(`Login response status: ${loginRes.status}`);
|
||||
const setCookieHeaders = loginRes.headers.getSetCookie();
|
||||
console.log("Login response set-cookie headers:", setCookieHeaders);
|
||||
|
||||
let sessionCookie = "";
|
||||
for (const cookie of setCookieHeaders) {
|
||||
if (cookie.startsWith("session=")) {
|
||||
sessionCookie = cookie.split(";")[0];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!sessionCookie) {
|
||||
if (loginRes.status === 200) {
|
||||
console.log("No new session cookie returned, using guest cookie as session cookie.");
|
||||
sessionCookie = guestCookie;
|
||||
} else {
|
||||
const htmlText = await loginRes.text();
|
||||
console.log(`Login failed. Response body length: ${htmlText.length}`);
|
||||
// Log some body context if we can find error messages
|
||||
if (htmlText.includes("Invalid password") || htmlText.includes("error")) {
|
||||
console.log("Found error indicator in body. HTML preview:", htmlText.substring(0, 1000));
|
||||
} else {
|
||||
console.log("HTML preview:", htmlText.substring(0, 500));
|
||||
}
|
||||
throw new Error(`Failed to get session cookie from login (Status: ${loginRes.status}).`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Authenticated successfully! Connecting WebSocket...");
|
||||
socket = io(getFoundryUrl("/"), {
|
||||
forceNew: true,
|
||||
multiplex: false,
|
||||
transports: ["websocket", "polling"],
|
||||
extraHeaders: {
|
||||
Cookie: sessionCookie
|
||||
}
|
||||
});
|
||||
socket.on("connect", () => {
|
||||
console.log(`WebSocket connected to Foundry VTT world "${currentWorld}"!`);
|
||||
fetchActors().catch(err => console.error("Error fetching actors:", err));
|
||||
});
|
||||
socket.on("disconnect", () => {
|
||||
console.log("WebSocket disconnected.");
|
||||
});
|
||||
socket.on("createActor", (actor: any) => {
|
||||
console.log(`Actor created in Foundry: ${actor._id || actor.id}`);
|
||||
fetchActors().catch(err => console.error("Error fetching actors on create:", err));
|
||||
});
|
||||
socket.on("updateActor", (actor: any) => {
|
||||
console.log(`Actor updated in Foundry: ${actor._id || actor.id}`);
|
||||
fetchActors().catch(err => console.error("Error fetching actors on update:", err));
|
||||
});
|
||||
socket.on("deleteActor", (actor: any) => {
|
||||
console.log(`Actor deleted in Foundry: ${actor._id || actor.id}`);
|
||||
fetchActors().catch(err => console.error("Error fetching actors on delete:", err));
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error("Error connecting to Foundry:", err.message);
|
||||
console.log("Retrying in 10s...");
|
||||
setTimeout(connectToFoundry, 10000);
|
||||
}
|
||||
}
|
||||
|
||||
function fetchActors(): Promise<any[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!socket) {
|
||||
reject(new Error("WebSocket offline"));
|
||||
return;
|
||||
}
|
||||
const req = {
|
||||
type: "Actor",
|
||||
action: "get",
|
||||
operation: { query: {} }
|
||||
};
|
||||
socket.emit("modifyDocument", req, (response: any) => {
|
||||
if (response && response.result) {
|
||||
const mapping = getActiveMapping();
|
||||
actorsList = response.result.map((actor: any) => {
|
||||
if (actor.system) {
|
||||
const hp = mapping.getHP(actor);
|
||||
const level = mapping.getLevel(actor);
|
||||
actor.system.attributes = actor.system.attributes || {};
|
||||
actor.system.attributes.hp = hp;
|
||||
actor.system.details = actor.system.details || {};
|
||||
actor.system.details.level = level;
|
||||
}
|
||||
return actor;
|
||||
});
|
||||
console.log(`Fetched ${actorsList.length} actors.`);
|
||||
writeActorsToDat();
|
||||
resolve(actorsList);
|
||||
} else {
|
||||
console.log("Failed to fetch actors. Response:", JSON.stringify(response));
|
||||
reject(new Error("Failed to fetch actors"));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function writeActorsToDat() {
|
||||
try {
|
||||
const mapping = getActiveMapping();
|
||||
const lines = actorsList.map(actor => {
|
||||
const id = (actor._id || "").padEnd(16).substring(0, 16);
|
||||
const name = (actor.name || "").padEnd(20).substring(0, 20);
|
||||
const type = (actor.type || "").padEnd(10).substring(0, 10);
|
||||
|
||||
const hp = mapping.getHP(actor);
|
||||
const hpCur = String(hp.value).padStart(3, "0").substring(0, 3);
|
||||
const hpMax = String(hp.max).padStart(3, "0").substring(0, 3);
|
||||
|
||||
const lvlVal = mapping.getLevel(actor);
|
||||
const level = String(lvlVal).padStart(2, "0").substring(0, 2);
|
||||
|
||||
const padding = "".padEnd(25);
|
||||
return `${id}${name}${type}${hpCur}${hpMax}${level}${padding}`;
|
||||
});
|
||||
|
||||
fs.writeFileSync(ACTORS_FILE, lines.join("\n") + "\n", "utf8");
|
||||
console.log(`Synced actors to ${ACTORS_FILE}`);
|
||||
} catch (err) {
|
||||
console.error("Error writing ACTORS.DAT:", err);
|
||||
}
|
||||
}
|
||||
|
||||
function startWatchingCobolUpdates() {
|
||||
console.log(`Watching ${UPDATES_FILE}...`);
|
||||
if (!fs.existsSync(UPDATES_FILE)) {
|
||||
fs.writeFileSync(UPDATES_FILE, "", "utf8");
|
||||
}
|
||||
|
||||
let fsWait = false;
|
||||
fs.watch(UPDATES_FILE, (eventType) => {
|
||||
if (eventType === "change") {
|
||||
if (fsWait) return;
|
||||
fsWait = true;
|
||||
setTimeout(() => {
|
||||
fsWait = false;
|
||||
processCobolUpdates();
|
||||
}, 200);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function processCobolUpdates() {
|
||||
try {
|
||||
if (!fs.existsSync(UPDATES_FILE)) return;
|
||||
const content = fs.readFileSync(UPDATES_FILE, "utf8");
|
||||
if (!content.trim()) return;
|
||||
|
||||
const lines = content.split("\n");
|
||||
const updatesToSend: any[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.length < 54) continue;
|
||||
const id = line.substring(0, 16).trim();
|
||||
const name = line.substring(16, 36).trim();
|
||||
const type = line.substring(36, 46).trim();
|
||||
const hpCur = parseInt(line.substring(46, 49), 10);
|
||||
const hpMax = parseInt(line.substring(49, 52), 10);
|
||||
const level = parseInt(line.substring(52, 54), 10);
|
||||
|
||||
if (!id) continue;
|
||||
|
||||
const mapping = getActiveMapping();
|
||||
const actorSystem: any = {};
|
||||
mapping.setHP({ value: hpCur, max: hpMax }, actorSystem);
|
||||
mapping.setLevel(level, actorSystem);
|
||||
|
||||
const update: any = {
|
||||
_id: id,
|
||||
name: name,
|
||||
system: actorSystem
|
||||
};
|
||||
updatesToSend.push(update);
|
||||
}
|
||||
|
||||
if (updatesToSend.length > 0 && socket) {
|
||||
console.log(`Sending updates from COBOL updates to Foundry...`);
|
||||
const req = {
|
||||
type: "Actor",
|
||||
action: "update",
|
||||
operation: {
|
||||
updates: updatesToSend
|
||||
}
|
||||
};
|
||||
socket.emit("modifyDocument", req, async (res: any) => {
|
||||
console.log("Foundry update response received.");
|
||||
try {
|
||||
await fetchActors();
|
||||
} catch (err) {
|
||||
console.error("Error refetching actors after COBOL update:", err);
|
||||
}
|
||||
fs.writeFileSync(UPDATES_FILE, "", "utf8");
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error processing COBOL updates:", err);
|
||||
}
|
||||
}
|
||||
|
||||
async function getAdminSessionCookie(): Promise<string> {
|
||||
if (!FOUNDRY_ADMIN_KEY) {
|
||||
throw new Error("FOUNDRY_ADMIN_KEY is not configured in backend environment.");
|
||||
}
|
||||
|
||||
const res = await fetch(getFoundryUrl("/setup"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: "adminPassword",
|
||||
adminPassword: FOUNDRY_ADMIN_KEY
|
||||
}),
|
||||
redirect: "manual",
|
||||
});
|
||||
|
||||
if (res.status === 403) {
|
||||
throw new Error("Invalid Administrator Access Key (check FOUNDRY_ADMIN_KEY in .env.backend).");
|
||||
}
|
||||
|
||||
const cookies = res.headers.getSetCookie();
|
||||
let sessionCookie = "";
|
||||
for (const cookie of cookies) {
|
||||
if (cookie.startsWith("session=")) {
|
||||
sessionCookie = cookie.split(";")[0];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!sessionCookie) {
|
||||
throw new Error("Failed to get session cookie from setup login.");
|
||||
}
|
||||
|
||||
return sessionCookie;
|
||||
}
|
||||
|
||||
function readWorldsCache(): any[] {
|
||||
try {
|
||||
if (fs.existsSync(WORLDS_CACHE_FILE)) {
|
||||
const content = fs.readFileSync(WORLDS_CACHE_FILE, "utf8");
|
||||
return JSON.parse(content);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error reading worlds cache:", err);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function writeWorldsCache(worlds: any[]) {
|
||||
try {
|
||||
fs.writeFileSync(WORLDS_CACHE_FILE, JSON.stringify(worlds, null, 2), "utf8");
|
||||
console.log("Worlds list cached successfully.");
|
||||
} catch (err) {
|
||||
console.error("Error writing worlds cache:", err);
|
||||
}
|
||||
}
|
||||
|
||||
async function getActiveWorldFromJoin(): Promise<{ id: string; title: string } | null> {
|
||||
try {
|
||||
const joinPageRes = await fetch(getFoundryUrl("/join"));
|
||||
if (joinPageRes.url.includes("/setup")) {
|
||||
return null;
|
||||
}
|
||||
const cookies = joinPageRes.headers.getSetCookie();
|
||||
let guestCookie = "";
|
||||
for (const c of cookies) {
|
||||
if (c.startsWith("session=")) {
|
||||
guestCookie = c.split(";")[0];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!guestCookie) return null;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const tempSocket = io(getFoundryUrl("/"), {
|
||||
forceNew: true,
|
||||
multiplex: false,
|
||||
transports: ["websocket", "polling"],
|
||||
extraHeaders: {
|
||||
Cookie: guestCookie
|
||||
},
|
||||
timeout: 4000
|
||||
});
|
||||
|
||||
tempSocket.on("connect", () => {
|
||||
tempSocket.emit("getJoinData", (data: any) => {
|
||||
tempSocket.disconnect();
|
||||
if (data && data.world) {
|
||||
resolve({
|
||||
id: data.world.id,
|
||||
title: data.world.title,
|
||||
system: data.world.system
|
||||
});
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
tempSocket.on("connect_error", () => {
|
||||
tempSocket.disconnect();
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error in getActiveWorldFromJoin:", err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleWorldsListFallback(originalError: any): Promise<any[]> {
|
||||
const activeWorld = await getActiveWorldFromJoin();
|
||||
if (activeWorld) {
|
||||
console.log(`handleWorldsListFallback: Detected active world "${activeWorld.id}" from join page.`);
|
||||
currentWorldTitle = activeWorld.title;
|
||||
currentSystem = activeWorld.system || "";
|
||||
const cached = readWorldsCache();
|
||||
if (cached && cached.length > 0) {
|
||||
if (!cached.find((w: any) => w.id === activeWorld.id)) {
|
||||
cached.push({ id: activeWorld.id, title: activeWorld.title });
|
||||
writeWorldsCache(cached);
|
||||
}
|
||||
return cached.map((w: any) => ({
|
||||
id: w.id,
|
||||
title: w.title,
|
||||
active: w.id === activeWorld.id
|
||||
}));
|
||||
} else {
|
||||
const singleWorldList = [{ id: activeWorld.id, title: activeWorld.title, active: true }];
|
||||
writeWorldsCache([{ id: activeWorld.id, title: activeWorld.title }]);
|
||||
return singleWorldList;
|
||||
}
|
||||
} else {
|
||||
const cached = readWorldsCache();
|
||||
if (cached && cached.length > 0) {
|
||||
console.log("handleWorldsListFallback: No active world detected, returning cached worlds list with active: false.");
|
||||
return cached.map((w: any) => ({
|
||||
id: w.id,
|
||||
title: w.title,
|
||||
active: false
|
||||
}));
|
||||
}
|
||||
throw originalError || new Error("No worlds data returned from setup socket");
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchWorldsList(): Promise<any[]> {
|
||||
console.log("fetchWorldsList: Authenticating and getting setup cookie...");
|
||||
let cookie: string;
|
||||
try {
|
||||
cookie = await getAdminSessionCookie();
|
||||
} catch (err: any) {
|
||||
console.error("fetchWorldsList: Failed to get admin session cookie:", err.message);
|
||||
return await handleWorldsListFallback(err);
|
||||
}
|
||||
|
||||
console.log("fetchWorldsList: Cookie retrieved successfully. Connecting setup socket...");
|
||||
|
||||
try {
|
||||
const worlds = await new Promise<any[]>((resolve, reject) => {
|
||||
const socketSetup = io(getFoundryUrl("/"), {
|
||||
forceNew: true,
|
||||
multiplex: false,
|
||||
transports: ["websocket", "polling"],
|
||||
extraHeaders: {
|
||||
Cookie: cookie
|
||||
},
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
socketSetup.on("connect", () => {
|
||||
console.log("fetchWorldsList: Setup socket connected. Emitting getSetupData...");
|
||||
socketSetup.emit("getSetupData", (data: any) => {
|
||||
socketSetup.disconnect();
|
||||
if (data && data.worlds) {
|
||||
const activeWorldId = data.options?.world || null;
|
||||
console.log(`fetchWorldsList: Found ${data.worlds.length} worlds. Active world ID: ${activeWorldId}`);
|
||||
const worldsList = data.worlds.map((w: any) => ({
|
||||
id: w.id,
|
||||
title: w.title,
|
||||
active: w.id === activeWorldId
|
||||
}));
|
||||
const activeWorld = worldsList.find(w => w.active);
|
||||
if (activeWorld) {
|
||||
currentWorldTitle = activeWorld.title;
|
||||
}
|
||||
writeWorldsCache(worldsList.map((w: any) => ({ id: w.id, title: w.title })));
|
||||
resolve(worldsList);
|
||||
} else {
|
||||
console.log("fetchWorldsList: No worlds data found in response. Full data keys:", data ? Object.keys(data) : "null");
|
||||
reject(new Error("No worlds data returned from setup socket"));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
socketSetup.on("connect_error", (err) => {
|
||||
console.error("fetchWorldsList: Setup socket connection error:", err.message);
|
||||
socketSetup.disconnect();
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
return worlds;
|
||||
} catch (err) {
|
||||
console.log("fetchWorldsList: Setup socket failed or returned empty. Attempting fallback...");
|
||||
return await handleWorldsListFallback(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function getActiveGMCookie(): Promise<string> {
|
||||
const joinPageRes = await fetch(getFoundryUrl("/join"));
|
||||
if (joinPageRes.url.includes("/setup")) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const cookies = joinPageRes.headers.getSetCookie();
|
||||
let guestCookie = "";
|
||||
for (const c of cookies) {
|
||||
if (c.startsWith("session=")) {
|
||||
guestCookie = c.split(";")[0];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!guestCookie) {
|
||||
throw new Error("Failed to get guest session cookie for GM login.");
|
||||
}
|
||||
|
||||
const userId = await fetchUserId(guestCookie);
|
||||
|
||||
const postData = {
|
||||
userid: userId,
|
||||
password: currentPassword,
|
||||
action: "join"
|
||||
};
|
||||
|
||||
const loginRes = await fetch(getFoundryUrl("/join"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Cookie: guestCookie
|
||||
},
|
||||
body: JSON.stringify(postData),
|
||||
redirect: "manual"
|
||||
});
|
||||
|
||||
const setCookieHeaders = loginRes.headers.getSetCookie();
|
||||
let sessionCookie = "";
|
||||
for (const cookie of setCookieHeaders) {
|
||||
if (cookie.startsWith("session=")) {
|
||||
sessionCookie = cookie.split(";")[0];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!sessionCookie) {
|
||||
if (loginRes.status === 200) {
|
||||
console.log("No new session cookie returned for GM, using guest cookie as session cookie.");
|
||||
sessionCookie = guestCookie;
|
||||
} else {
|
||||
console.error(`GM login failed with status ${loginRes.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
return sessionCookie;
|
||||
}
|
||||
|
||||
async function launchWorld(worldId: string) {
|
||||
const cookie = await getAdminSessionCookie();
|
||||
|
||||
const res = await fetch(getFoundryUrl("/setup"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Cookie: cookie,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: "launchWorld",
|
||||
world: worldId
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to launch world ${worldId}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function shutdownWorld() {
|
||||
console.log("Shutting down active world...");
|
||||
let cookie = "";
|
||||
try {
|
||||
cookie = await getActiveGMCookie();
|
||||
} catch (err: any) {
|
||||
console.log("Could not get GM cookie for shutdown (setup might be active):", err.message);
|
||||
}
|
||||
|
||||
if (!cookie) {
|
||||
console.log("Attempting shutdown using Admin access...");
|
||||
cookie = await getAdminSessionCookie();
|
||||
}
|
||||
|
||||
const res = await fetch(getFoundryUrl("/setup"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Cookie: cookie,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
shutdown: true
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to shut down active world. Status: ${res.status}`);
|
||||
}
|
||||
console.log("World shutdown successfully completed.");
|
||||
}
|
||||
|
||||
Bun.serve({
|
||||
port: PORT,
|
||||
async fetch(req) {
|
||||
const url = new URL(req.url);
|
||||
const headers = {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type",
|
||||
"Content-Type": "application/json"
|
||||
};
|
||||
|
||||
if (req.method === "OPTIONS") {
|
||||
return new Response(null, { headers });
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/actors" && req.method === "GET") {
|
||||
return new Response(JSON.stringify(actorsList), { headers });
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/actors/refetch" && req.method === "POST") {
|
||||
try {
|
||||
if (socket) {
|
||||
const list = await fetchActors();
|
||||
return new Response(JSON.stringify(list), { headers });
|
||||
} else {
|
||||
return new Response(JSON.stringify({ error: "WebSocket offline" }), { status: 503, headers });
|
||||
}
|
||||
} catch (err: any) {
|
||||
return new Response(JSON.stringify({ error: err.message }), { status: 500, headers });
|
||||
}
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/actors/update" && req.method === "POST") {
|
||||
try {
|
||||
const body: any = await req.json();
|
||||
const { id, updates } = body;
|
||||
|
||||
if (!id || !updates) {
|
||||
return new Response(JSON.stringify({ error: "Missing parameters" }), { status: 400, headers });
|
||||
}
|
||||
|
||||
if (socket) {
|
||||
if (updates.system) {
|
||||
const mapping = getActiveMapping();
|
||||
const hpVal = updates.system.attributes?.hp?.value;
|
||||
const hpMax = updates.system.attributes?.hp?.max;
|
||||
const level = updates.system.details?.level;
|
||||
|
||||
const actorSystem: any = {};
|
||||
if (hpVal !== undefined && hpMax !== undefined) {
|
||||
mapping.setHP({ value: hpVal, max: hpMax }, actorSystem);
|
||||
}
|
||||
if (level !== undefined) {
|
||||
mapping.setLevel(level, actorSystem);
|
||||
}
|
||||
updates.system = actorSystem;
|
||||
}
|
||||
|
||||
const reqObj = {
|
||||
type: "Actor",
|
||||
action: "update",
|
||||
operation: {
|
||||
updates: [{ _id: id, ...updates }]
|
||||
}
|
||||
};
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
socket.emit("modifyDocument", reqObj, async (res: any) => {
|
||||
console.log(`Updated Actor ${id} via REST API. Response:`, JSON.stringify(res));
|
||||
try {
|
||||
await fetchActors();
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
return new Response(JSON.stringify({ success: true }), { headers });
|
||||
} else {
|
||||
return new Response(JSON.stringify({ error: "WebSocket offline" }), { status: 503, headers });
|
||||
}
|
||||
} catch (err: any) {
|
||||
return new Response(JSON.stringify({ error: err.message }), { status: 500, headers });
|
||||
}
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/worlds" && req.method === "GET") {
|
||||
try {
|
||||
const worlds = await fetchWorldsList();
|
||||
return new Response(JSON.stringify(worlds), { headers });
|
||||
} catch (err: any) {
|
||||
return new Response(JSON.stringify({ error: err.message }), { status: 500, headers });
|
||||
}
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/worlds/launch" && req.method === "POST") {
|
||||
try {
|
||||
const { worldId } = await req.json();
|
||||
if (!worldId) {
|
||||
return new Response(JSON.stringify({ error: "Missing worldId" }), { status: 400, headers });
|
||||
}
|
||||
|
||||
// Find if there's currently an active world
|
||||
const worlds = await fetchWorldsList();
|
||||
const activeWorld = worlds.find(w => w.active);
|
||||
if (activeWorld) {
|
||||
console.log(`Shutting down active world ${activeWorld.id} first...`);
|
||||
await shutdownWorld();
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
}
|
||||
|
||||
console.log(`Launching world ${worldId}...`);
|
||||
await launchWorld(worldId);
|
||||
await new Promise(r => setTimeout(r, 3000));
|
||||
|
||||
currentWorld = worldId;
|
||||
connectToFoundry();
|
||||
|
||||
return new Response(JSON.stringify({ success: true }), { headers });
|
||||
} catch (err: any) {
|
||||
return new Response(JSON.stringify({ error: err.message }), { status: 500, headers });
|
||||
}
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/worlds/shutdown" && req.method === "POST") {
|
||||
try {
|
||||
console.log("Shutting down active world...");
|
||||
await shutdownWorld();
|
||||
disconnectFromFoundry();
|
||||
currentWorld = "";
|
||||
return new Response(JSON.stringify({ success: true }), { headers });
|
||||
} catch (err: any) {
|
||||
return new Response(JSON.stringify({ error: err.message }), { status: 500, headers });
|
||||
}
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/worlds/select" && req.method === "POST") {
|
||||
try {
|
||||
const body: any = await req.json();
|
||||
const { worldId, user, password } = body;
|
||||
if (!worldId) {
|
||||
return new Response(JSON.stringify({ error: "Missing worldId" }), { status: 400, headers });
|
||||
}
|
||||
|
||||
console.log(`Selecting world targeting: ${worldId}`);
|
||||
currentWorld = worldId;
|
||||
if (user) currentUser = user;
|
||||
if (password !== undefined) currentPassword = password;
|
||||
|
||||
connectToFoundry();
|
||||
return new Response(JSON.stringify({ success: true, world: currentWorld, user: currentUser }), { headers });
|
||||
} catch (err: any) {
|
||||
return new Response(JSON.stringify({ error: err.message }), { status: 500, headers });
|
||||
}
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/packages/update" && req.method === "POST") {
|
||||
try {
|
||||
const body: any = await req.json();
|
||||
const { manifest, type } = body;
|
||||
if (!manifest) {
|
||||
return new Response(JSON.stringify({ error: "Missing manifest URL" }), { status: 400, headers });
|
||||
}
|
||||
|
||||
const activeWorld = await getActiveWorldFromJoin();
|
||||
if (activeWorld) {
|
||||
return new Response(JSON.stringify({ error: `A world (${activeWorld.title}) is currently active. Please shut down the world before updating packages.` }), { status: 400, headers });
|
||||
}
|
||||
|
||||
console.log(`Installing/updating package of type ${type || "system"} from manifest: ${manifest}`);
|
||||
const cookie = await getAdminSessionCookie();
|
||||
|
||||
const res = await fetch(getFoundryUrl("/setup"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Cookie: cookie,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: "installPackage",
|
||||
type: type || "system",
|
||||
manifest: manifest
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const errMsg = await res.text();
|
||||
throw new Error(`Foundry Setup returned status ${res.status}: ${errMsg}`);
|
||||
}
|
||||
|
||||
const resData = await res.json();
|
||||
console.log("Package update response:", resData);
|
||||
|
||||
return new Response(JSON.stringify({ success: true, details: resData }), { headers });
|
||||
} catch (err: any) {
|
||||
return new Response(JSON.stringify({ error: err.message }), { status: 500, headers });
|
||||
}
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/worlds/active" && req.method === "GET") {
|
||||
return new Response(JSON.stringify({
|
||||
world: currentWorld,
|
||||
worldTitle: currentWorldTitle || currentWorld,
|
||||
system: currentSystem,
|
||||
user: currentUser,
|
||||
connected: socket?.connected || false
|
||||
}), { headers });
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ error: "Not Found" }), { status: 404, headers });
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Proxy Backend listening on port ${PORT}`);
|
||||
connectToFoundry();
|
||||
startWatchingCobolUpdates();
|
||||
@@ -0,0 +1,34 @@
|
||||
import { fetch } from "bun";
|
||||
|
||||
async function run() {
|
||||
const url = "https://foundry.octoturge.com/scripts/foundry.mjs";
|
||||
console.log("Fetching:", url);
|
||||
const res = await fetch(url);
|
||||
const text = await res.text();
|
||||
console.log("Length:", text.length);
|
||||
|
||||
// Let's search for where socket connection is initialized or used during Setup
|
||||
// Search for: io( or connect( or socket
|
||||
const queries = [
|
||||
/connect\s*\(\s*socket/i,
|
||||
/socket\s*=\s*io/i,
|
||||
/io\s*\(/i,
|
||||
/class Setup/i,
|
||||
/setup\.json/i,
|
||||
/setupData/i,
|
||||
/getSetupData/i,
|
||||
/setup-menu/i
|
||||
];
|
||||
|
||||
for (const q of queries) {
|
||||
console.log(`=== Matches for ${q} ===`);
|
||||
const regex = new RegExp(`.{0,100}${q.source}.{0,100}`, "gi");
|
||||
const matches = text.match(regex);
|
||||
console.log(`Count: ${matches ? matches.length : 0}`);
|
||||
if (matches) {
|
||||
matches.slice(0, 5).forEach(m => console.log(m.trim()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -0,0 +1,80 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!-- Page Metadata -->
|
||||
<title>Critical Failure!</title>
|
||||
<meta name="description" content="Foundry Virtual Tabletop - A Self-Hosted & Modern Role-playing Platform">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<link rel="icon" href="icons/vtt.png">
|
||||
|
||||
<!-- Included Stylesheets -->
|
||||
<link href="css/foundry2.css" rel="stylesheet" type="text/css" media="all">
|
||||
|
||||
<style>
|
||||
@import "fonts/fontawesome/css/all.min.css" layer(variables);
|
||||
</style>
|
||||
|
||||
<!-- Included Scripts -->
|
||||
<script src="" defer></script>
|
||||
<script src="" defer></script>
|
||||
<script src="" defer></script>
|
||||
<script src="" defer></script>
|
||||
<script src="" defer></script>
|
||||
<script src="" defer></script>
|
||||
<script src="" defer></script>
|
||||
<script src="" defer></script>
|
||||
<script src="" defer></script>
|
||||
<script src="" defer></script>
|
||||
<script src="" defer></script>
|
||||
<script src="" defer></script>
|
||||
<script src="" defer></script>
|
||||
<script src="" defer></script>
|
||||
|
||||
<!-- Inline Scripts -->
|
||||
<script>
|
||||
const SIGNED_EULA=true;
|
||||
const ROUTE_PREFIX="";
|
||||
const MESSAGES=null;
|
||||
</script>
|
||||
|
||||
<!-- Layer System & Module Styles -->
|
||||
<style>
|
||||
</style>
|
||||
|
||||
<!-- Inline Styles -->
|
||||
</head>
|
||||
|
||||
<body class="auth error flexcol theme-dark">
|
||||
<div id="main-background"></div>
|
||||
|
||||
<!-- Page Header -->
|
||||
<header id="main-header" class="flexcol">
|
||||
<h1>Critical Failure!</h1>
|
||||
</header>
|
||||
|
||||
<!-- Body Content -->
|
||||
<article id="error" class="application framed standard-form">
|
||||
<h2 class="border">Foundry Virtual Tabletop</h2>
|
||||
|
||||
<div class="error-details">
|
||||
<p>There is currently no active game session. Please wait for the host to configure the world and then refresh this page.</p>
|
||||
</div>
|
||||
|
||||
<footer class="form-footer">
|
||||
<a class="button" href="/setup" target="_self">
|
||||
<i class="fa-solid fa-backward" inert></i> Go Back
|
||||
</a>
|
||||
</footer>
|
||||
</article>
|
||||
|
||||
<!-- Footer Watermark -->
|
||||
<footer id="watermark" class="flexcol">
|
||||
<p id="software-version">Version 14 Build 363</p>
|
||||
</footer>
|
||||
|
||||
<!-- Global Tooltip Element -->
|
||||
<aside id="tooltip" role="tooltip" popover="manual"></aside>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
Reference in New Issue
Block a user