import { MODULE_ID } from './constants.js';
import { FoundryDataAccess } from './data-access.js';
import { ComfyUIManager } from './comfyui-manager.js';
export class QueryHandlers {
public dataAccess: FoundryDataAccess;
private comfyuiManager: ComfyUIManager;
constructor() {
this.dataAccess = new FoundryDataAccess();
this.comfyuiManager = new ComfyUIManager();
}
/**
* SECURITY: Validate GM access - returns silent failure for non-GM users
*/
private validateGMAccess(): { allowed: boolean; error?: any } {
if (!game.user?.isGM) {
// Silent failure - no error message for non-GM users
return { allowed: false };
}
return { allowed: true };
}
/**
* Register all query handlers in CONFIG.queries
*/
registerHandlers(): void {
const modulePrefix = MODULE_ID;
// Character/Actor queries
CONFIG.queries[`${modulePrefix}.getCharacterInfo`] = this.handleGetCharacterInfo.bind(this);
CONFIG.queries[`${modulePrefix}.listActors`] = this.handleListActors.bind(this);
// Compendium queries
CONFIG.queries[`${modulePrefix}.searchCompendium`] = this.handleSearchCompendium.bind(this);
CONFIG.queries[`${modulePrefix}.listCreaturesByCriteria`] = this.handleListCreaturesByCriteria.bind(this);
CONFIG.queries[`${modulePrefix}.getAvailablePacks`] = this.handleGetAvailablePacks.bind(this);
// Scene queries
CONFIG.queries[`${modulePrefix}.getActiveScene`] = this.handleGetActiveScene.bind(this);
CONFIG.queries[`${modulePrefix}.list-scenes`] = this.handleListScenes.bind(this);
CONFIG.queries[`${modulePrefix}.switch-scene`] = this.handleSwitchScene.bind(this);
// World queries
CONFIG.queries[`${modulePrefix}.getWorldInfo`] = this.handleGetWorldInfo.bind(this);
// Utility queries
CONFIG.queries[`${modulePrefix}.ping`] = this.handlePing.bind(this);
// Phase 2 & 3: Write operation queries
CONFIG.queries[`${modulePrefix}.createActorFromCompendium`] = this.handleCreateActorFromCompendium.bind(this);
CONFIG.queries[`${modulePrefix}.getCompendiumDocumentFull`] = this.handleGetCompendiumDocumentFull.bind(this);
CONFIG.queries[`${modulePrefix}.addActorsToScene`] = this.handleAddActorsToScene.bind(this);
CONFIG.queries[`${modulePrefix}.validateWritePermissions`] = this.handleValidateWritePermissions.bind(this);
CONFIG.queries[`${modulePrefix}.createJournalEntry`] = this.handleCreateJournalEntry.bind(this);
CONFIG.queries[`${modulePrefix}.listJournals`] = this.handleListJournals.bind(this);
CONFIG.queries[`${modulePrefix}.getJournalContent`] = this.handleGetJournalContent.bind(this);
CONFIG.queries[`${modulePrefix}.updateJournalContent`] = this.handleUpdateJournalContent.bind(this);
// Phase 4: Dice roll queries
CONFIG.queries[`${modulePrefix}.request-player-rolls`] = this.handleRequestPlayerRolls.bind(this);
// Enhanced creature index for campaign analysis
CONFIG.queries[`${modulePrefix}.getEnhancedCreatureIndex`] = this.handleGetEnhancedCreatureIndex.bind(this);
// Campaign management queries
CONFIG.queries[`${modulePrefix}.updateCampaignProgress`] = this.handleUpdateCampaignProgress.bind(this);
// Phase 6: Actor ownership management
CONFIG.queries[`${modulePrefix}.setActorOwnership`] = this.handleSetActorOwnership.bind(this);
CONFIG.queries[`${modulePrefix}.getActorOwnership`] = this.handleGetActorOwnership.bind(this);
CONFIG.queries[`${modulePrefix}.getFriendlyNPCs`] = this.handleGetFriendlyNPCs.bind(this);
CONFIG.queries[`${modulePrefix}.getPartyCharacters`] = this.handleGetPartyCharacters.bind(this);
CONFIG.queries[`${modulePrefix}.getConnectedPlayers`] = this.handleGetConnectedPlayers.bind(this);
CONFIG.queries[`${modulePrefix}.findPlayers`] = this.handleFindPlayers.bind(this);
CONFIG.queries[`${modulePrefix}.findActor`] = this.handleFindActor.bind(this);
// Token manipulation queries
CONFIG.queries[`${modulePrefix}.moveToken`] = this.handleMoveToken.bind(this);
CONFIG.queries[`${modulePrefix}.updateToken`] = this.handleUpdateToken.bind(this);
CONFIG.queries[`${modulePrefix}.deleteTokens`] = this.handleDeleteTokens.bind(this);
CONFIG.queries[`${modulePrefix}.getTokenDetails`] = this.handleGetTokenDetails.bind(this);
CONFIG.queries[`${modulePrefix}.toggleTokenCondition`] = this.handleToggleTokenCondition.bind(this);
CONFIG.queries[`${modulePrefix}.getAvailableConditions`] = this.handleGetAvailableConditions.bind(this);
// Map generation queries (hybrid architecture)
CONFIG.queries[`${modulePrefix}.generate-map`] = this.handleGenerateMap.bind(this);
CONFIG.queries[`${modulePrefix}.check-map-status`] = this.handleCheckMapStatus.bind(this);
CONFIG.queries[`${modulePrefix}.cancel-map-job`] = this.handleCancelMapJob.bind(this);
CONFIG.queries[`${modulePrefix}.upload-generated-map`] = this.handleUploadGeneratedMap.bind(this);
}
/**
* Unregister all query handlers
*/
unregisterHandlers(): void {
const modulePrefix = MODULE_ID;
const keysToRemove = Object.keys(CONFIG.queries).filter(key => key.startsWith(modulePrefix));
for (const key of keysToRemove) {
delete CONFIG.queries[key];
}
}
/**
* Handle query requests from other parts of the module
*/
async handleQuery(queryName: string, data: any): Promise<any> {
try {
const handler = CONFIG.queries[queryName];
if (!handler || typeof handler !== 'function') {
throw new Error(`Query handler not found: ${queryName}`);
}
return await handler(data);
} catch (error) {
console.error(`[${MODULE_ID}] Query failed: ${queryName}`, error);
return {
error: error instanceof Error ? error.message : 'Unknown error',
success: false
};
}
}
/**
* Handle character information request
*/
private async handleGetCharacterInfo(data: { characterName?: string; characterId?: string }): Promise<any> {
try {
// SECURITY: Silent GM validation
const gmCheck = this.validateGMAccess();
if (!gmCheck.allowed) {
return { error: 'Access denied', success: false };
}
this.dataAccess.validateFoundryState();
const identifier = data.characterName || data.characterId;
if (!identifier) {
throw new Error('characterName or characterId is required');
}
return await this.dataAccess.getCharacterInfo(identifier);
} catch (error) {
throw new Error(`Failed to get character info: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Handle list actors request
*/
private async handleListActors(data: { type?: string }): Promise<any> {
try {
// SECURITY: Silent GM validation
const gmCheck = this.validateGMAccess();
if (!gmCheck.allowed) {
return { error: 'Access denied', success: false };
}
this.dataAccess.validateFoundryState();
const actors = await this.dataAccess.listActors();
// Filter by type if specified
if (data.type) {
return actors.filter(actor => actor.type === data.type);
}
return actors;
} catch (error) {
throw new Error(`Failed to list actors: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Handle compendium search request
*/
private async handleSearchCompendium(data: {
query: string;
packType?: string;
filters?: {
challengeRating?: number | { min?: number; max?: number };
creatureType?: string;
size?: string;
alignment?: string;
hasLegendaryActions?: boolean;
spellcaster?: boolean;
}
}): Promise<any> {
try {
// SECURITY: Silent GM validation
const gmCheck = this.validateGMAccess();
if (!gmCheck.allowed) {
return { error: 'Access denied', success: false };
}
this.dataAccess.validateFoundryState();
// Add better parameter validation
if (!data || typeof data !== 'object') {
throw new Error('Invalid data parameter structure');
}
if (!data.query || typeof data.query !== 'string') {
throw new Error('query parameter is required and must be a string');
}
return await this.dataAccess.searchCompendium(data.query, data.packType, data.filters);
} catch (error) {
throw new Error(`Failed to search compendium: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Handle list creatures by criteria request
*/
private async handleListCreaturesByCriteria(data: {
challengeRating?: number | { min?: number; max?: number };
creatureType?: string;
size?: string;
hasSpells?: boolean;
hasLegendaryActions?: boolean;
limit?: number;
}): Promise<any> {
try {
// SECURITY: Silent GM validation
const gmCheck = this.validateGMAccess();
if (!gmCheck.allowed) {
return { error: 'Access denied', success: false };
}
this.dataAccess.validateFoundryState();
const result = await this.dataAccess.listCreaturesByCriteria(data);
// Handle the new format with search summary
return {
response: result
};
} catch (error) {
throw new Error(`Failed to list creatures by criteria: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Handle get available packs request
*/
private async handleGetAvailablePacks(): Promise<any> {
try {
// SECURITY: Silent GM validation
const gmCheck = this.validateGMAccess();
if (!gmCheck.allowed) {
return { error: 'Access denied', success: false };
}
this.dataAccess.validateFoundryState();
return await this.dataAccess.getAvailablePacks();
} catch (error) {
throw new Error(`Failed to get available packs: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Handle get active scene request
*/
private async handleGetActiveScene(): Promise<any> {
try {
// SECURITY: Silent GM validation
const gmCheck = this.validateGMAccess();
if (!gmCheck.allowed) {
return { error: 'Access denied', success: false };
}
this.dataAccess.validateFoundryState();
return await this.dataAccess.getActiveScene();
} catch (error) {
throw new Error(`Failed to get active scene: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Handle get world info request
*/
private async handleGetWorldInfo(): Promise<any> {
try {
// SECURITY: Silent GM validation
const gmCheck = this.validateGMAccess();
if (!gmCheck.allowed) {
return { error: 'Access denied', success: false };
}
this.dataAccess.validateFoundryState();
return await this.dataAccess.getWorldInfo();
} catch (error) {
throw new Error(`Failed to get world info: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Handle ping request
*/
private async handlePing(): Promise<any> {
return {
status: 'ok',
timestamp: Date.now(),
module: MODULE_ID,
foundryVersion: game.version,
worldId: game.world?.id,
userId: game.user?.id,
};
}
/**
* Get list of all registered query methods
*/
getRegisteredMethods(): string[] {
const modulePrefix = MODULE_ID;
return Object.keys(CONFIG.queries)
.filter(key => key.startsWith(modulePrefix))
.map(key => key.replace(`${modulePrefix}.`, ''));
}
/**
* Test if a specific query handler is registered
*/
isMethodRegistered(method: string): boolean {
const queryKey = `${MODULE_ID}.${method}`;
return queryKey in CONFIG.queries && typeof CONFIG.queries[queryKey] === 'function';
}
// ===== PHASE 2: WRITE OPERATION HANDLERS =====
/**
* Handle actor creation from specific compendium entry
*/
private async handleCreateActorFromCompendium(data: {
packId: string;
itemId: string;
customNames?: string[] | undefined;
quantity?: number | undefined;
addToScene?: boolean | undefined;
placement?: {
type: 'random' | 'grid' | 'center' | 'coordinates';
coordinates?: { x: number; y: number }[];
} | undefined;
}): Promise<any> {
try {
// SECURITY: Silent GM validation
const gmCheck = this.validateGMAccess();
if (!gmCheck.allowed) {
return { error: 'Access denied', success: false };
}
this.dataAccess.validateFoundryState();
// Clean interface - direct pack/item reference only
const requestData: any = {
packId: data.packId,
itemId: data.itemId,
customNames: data.customNames || [],
quantity: data.quantity || 1,
addToScene: data.addToScene || false,
};
if (data.placement) {
requestData.placement = data.placement;
}
return await this.dataAccess.createActorFromCompendiumEntry(requestData);
} catch (error) {
throw new Error(`Failed to create actor from compendium: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Handle get compendium document full request
*/
private async handleGetCompendiumDocumentFull(data: {
packId: string;
documentId: string;
}): Promise<any> {
try {
// SECURITY: Silent GM validation
const gmCheck = this.validateGMAccess();
if (!gmCheck.allowed) {
return { error: 'Access denied', success: false };
}
this.dataAccess.validateFoundryState();
if (!data.packId) {
throw new Error('packId is required');
}
if (!data.documentId) {
throw new Error('documentId is required');
}
return await this.dataAccess.getCompendiumDocumentFull(data.packId, data.documentId);
} catch (error) {
throw new Error(`Failed to get compendium document: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Handle add actors to scene request
*/
private async handleAddActorsToScene(data: {
actorIds: string[];
placement?: 'random' | 'grid' | 'center';
hidden?: boolean;
}): Promise<any> {
try {
// SECURITY: Silent GM validation
const gmCheck = this.validateGMAccess();
if (!gmCheck.allowed) {
return { error: 'Access denied', success: false };
}
this.dataAccess.validateFoundryState();
if (!data.actorIds || !Array.isArray(data.actorIds) || data.actorIds.length === 0) {
throw new Error('actorIds array is required and must not be empty');
}
return await this.dataAccess.addActorsToScene({
actorIds: data.actorIds,
placement: data.placement || 'random',
hidden: data.hidden || false,
});
} catch (error) {
throw new Error(`Failed to add actors to scene: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Handle validate write permissions request
*/
private async handleValidateWritePermissions(data: {
operation: 'createActor' | 'modifyScene';
}): Promise<any> {
try {
// SECURITY: Silent GM validation
const gmCheck = this.validateGMAccess();
if (!gmCheck.allowed) {
return { error: 'Access denied', success: false };
}
this.dataAccess.validateFoundryState();
if (!data.operation) {
throw new Error('operation is required');
}
return await this.dataAccess.validateWritePermissions(data.operation);
} catch (error) {
throw new Error(`Failed to validate write permissions: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Handle journal entry creation
*/
async handleCreateJournalEntry(data: any): Promise<any> {
try {
// SECURITY: Silent GM validation
const gmCheck = this.validateGMAccess();
if (!gmCheck.allowed) {
return { error: 'Access denied', success: false };
}
if (!data.name) {
throw new Error('name is required');
}
if (!data.content) {
throw new Error('content is required');
}
return await this.dataAccess.createJournalEntry({
name: data.name,
content: data.content,
});
} catch (error) {
throw new Error(`Failed to create journal entry: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Handle list journals request
*/
async handleListJournals(): Promise<any> {
try {
// SECURITY: Silent GM validation
const gmCheck = this.validateGMAccess();
if (!gmCheck.allowed) {
return { error: 'Access denied', success: false };
}
this.dataAccess.validateFoundryState();
return await this.dataAccess.listJournals();
} catch (error) {
throw new Error(`Failed to list journals: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Handle get journal content request
*/
async handleGetJournalContent(data: { journalId: string }): Promise<any> {
try {
// SECURITY: Silent GM validation
const gmCheck = this.validateGMAccess();
if (!gmCheck.allowed) {
return { error: 'Access denied', success: false };
}
this.dataAccess.validateFoundryState();
if (!data.journalId) {
throw new Error('journalId is required');
}
return await this.dataAccess.getJournalContent(data.journalId);
} catch (error) {
throw new Error(`Failed to get journal content: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Handle update journal content request
*/
async handleUpdateJournalContent(data: { journalId: string; content: string }): Promise<any> {
try {
// SECURITY: Silent GM validation
const gmCheck = this.validateGMAccess();
if (!gmCheck.allowed) {
return { error: 'Access denied', success: false };
}
this.dataAccess.validateFoundryState();
if (!data.journalId) {
throw new Error('journalId is required');
}
if (!data.content) {
throw new Error('content is required');
}
return await this.dataAccess.updateJournalContent({
journalId: data.journalId,
content: data.content,
});
} catch (error) {
throw new Error(`Failed to update journal content: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Handle request player rolls - creates interactive roll buttons in chat
*/
async handleRequestPlayerRolls(data: {
rollType: string;
rollTarget: string;
targetPlayer: string;
isPublic: boolean;
rollModifier: string;
flavor: string;
}): Promise<any> {
try {
// SECURITY: Silent GM validation
const gmCheck = this.validateGMAccess();
if (!gmCheck.allowed) {
return { error: 'Access denied', success: false };
}
this.dataAccess.validateFoundryState();
if (!data.rollType || !data.rollTarget || !data.targetPlayer) {
throw new Error('rollType, rollTarget, and targetPlayer are required');
}
return await this.dataAccess.requestPlayerRolls(data);
} catch (error) {
throw new Error(`Failed to request player rolls: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Handle get enhanced creature index request
*/
async handleGetEnhancedCreatureIndex(): Promise<any> {
try {
// SECURITY: Silent GM validation
const gmCheck = this.validateGMAccess();
if (!gmCheck.allowed) {
return { error: 'Access denied', success: false };
}
this.dataAccess.validateFoundryState();
return await this.dataAccess.getEnhancedCreatureIndex();
} catch (error) {
throw new Error(`Failed to get enhanced creature index: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Handle campaign progress update request
*/
async handleUpdateCampaignProgress(data: { campaignId: string; partId: string; newStatus: string }): Promise<any> {
try {
// SECURITY: Silent GM validation
const gmCheck = this.validateGMAccess();
if (!gmCheck.allowed) {
return { error: 'Access denied', success: false };
}
this.dataAccess.validateFoundryState();
// For now, this is a pass-through to the MCP server
// In the future, campaign data might be stored in Foundry world flags
// Currently, the campaign dashboard regeneration happens server-side
return {
success: true,
message: `Campaign progress updated: ${data.partId} is now ${data.newStatus}`,
campaignId: data.campaignId,
partId: data.partId,
newStatus: data.newStatus
};
} catch (error) {
throw new Error(`Failed to update campaign progress: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Handle set actor ownership request
*/
async handleSetActorOwnership(data: any): Promise<any> {
try {
// SECURITY: Silent GM validation
const gmCheck = this.validateGMAccess();
if (!gmCheck.allowed) {
return { error: 'Access denied', success: false };
}
this.dataAccess.validateFoundryState();
if (!data.actorId || !data.userId || data.permission === undefined) {
throw new Error('actorId, userId, and permission are required');
}
return await this.dataAccess.setActorOwnership(data);
} catch (error) {
throw new Error(`Failed to set actor ownership: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Handle get actor ownership request
*/
async handleGetActorOwnership(data: any): Promise<any> {
try {
// SECURITY: Silent GM validation
const gmCheck = this.validateGMAccess();
if (!gmCheck.allowed) {
return { error: 'Access denied', success: false };
}
this.dataAccess.validateFoundryState();
return await this.dataAccess.getActorOwnership(data);
} catch (error) {
throw new Error(`Failed to get actor ownership: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Handle get friendly NPCs request
*/
async handleGetFriendlyNPCs(): Promise<any> {
try {
// SECURITY: Silent GM validation
const gmCheck = this.validateGMAccess();
if (!gmCheck.allowed) {
return { error: 'Access denied', success: false };
}
this.dataAccess.validateFoundryState();
return await this.dataAccess.getFriendlyNPCs();
} catch (error) {
throw new Error(`Failed to get friendly NPCs: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Handle get party characters request
*/
async handleGetPartyCharacters(): Promise<any> {
try {
// SECURITY: Silent GM validation
const gmCheck = this.validateGMAccess();
if (!gmCheck.allowed) {
return { error: 'Access denied', success: false };
}
this.dataAccess.validateFoundryState();
return await this.dataAccess.getPartyCharacters();
} catch (error) {
throw new Error(`Failed to get party characters: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Handle get connected players request
*/
async handleGetConnectedPlayers(): Promise<any> {
try {
// SECURITY: Silent GM validation
const gmCheck = this.validateGMAccess();
if (!gmCheck.allowed) {
return { error: 'Access denied', success: false };
}
this.dataAccess.validateFoundryState();
return await this.dataAccess.getConnectedPlayers();
} catch (error) {
throw new Error(`Failed to get connected players: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Handle find players request
*/
async handleFindPlayers(data: any): Promise<any> {
try {
// SECURITY: Silent GM validation
const gmCheck = this.validateGMAccess();
if (!gmCheck.allowed) {
return { error: 'Access denied', success: false };
}
this.dataAccess.validateFoundryState();
if (!data.identifier) {
throw new Error('identifier is required');
}
return await this.dataAccess.findPlayers(data);
} catch (error) {
throw new Error(`Failed to find players: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Handle find actor request
*/
async handleFindActor(data: any): Promise<any> {
try {
// SECURITY: Silent GM validation
const gmCheck = this.validateGMAccess();
if (!gmCheck.allowed) {
return { error: 'Access denied', success: false };
}
this.dataAccess.validateFoundryState();
if (!data.identifier) {
throw new Error('identifier is required');
}
return await this.dataAccess.findActor(data);
} catch (error) {
throw new Error(`Failed to find actor: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Handle list scenes request
*/
private async handleListScenes(data: any): Promise<any> {
try {
// SECURITY: Silent GM validation
const gmCheck = this.validateGMAccess();
if (!gmCheck.allowed) {
return { error: 'Access denied', success: false };
}
this.dataAccess.validateFoundryState();
return await this.dataAccess.listScenes(data);
} catch (error) {
throw new Error(`Failed to list scenes: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Handle switch scene request
*/
private async handleSwitchScene(data: any): Promise<any> {
try {
// SECURITY: Silent GM validation
const gmCheck = this.validateGMAccess();
if (!gmCheck.allowed) {
return { error: 'Access denied', success: false };
}
this.dataAccess.validateFoundryState();
if (!data.scene_identifier) {
throw new Error('scene_identifier is required');
}
return await this.dataAccess.switchScene(data);
} catch (error) {
throw new Error(`Failed to switch scene: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Handle map generation request - uses hybrid architecture
*/
private async handleGenerateMap(data: any): Promise<any> {
try {
// SECURITY: Silent GM validation
const gmCheck = this.validateGMAccess();
if (!gmCheck.allowed) {
return { error: 'Access denied', success: false };
}
if (!data.prompt || typeof data.prompt !== 'string') {
throw new Error('Prompt is required and must be a string');
}
if (!data.scene_name || typeof data.scene_name !== 'string') {
throw new Error('Scene name is required and must be a string');
}
// Get quality setting from module settings
const quality = game.settings.get(MODULE_ID, 'mapGenQuality') || 'low';
const params = {
prompt: data.prompt.trim(),
scene_name: data.scene_name.trim(),
size: data.size || 'medium',
grid_size: data.grid_size || 70,
quality: quality
};
// Use ComfyUIManager to communicate with backend via WebSocket
const response = await this.comfyuiManager.generateMap(params);
const isSuccess = typeof response?.success === 'boolean' ? response.success : response?.status === 'success';
if (!isSuccess) {
const errorMessage = response?.error || response?.message || 'Map generation failed';
return {
error: errorMessage,
success: false,
status: response?.status ?? 'error'
};
}
return {
success: true,
status: response?.status ?? 'success',
jobId: response.jobId,
message: response.message || 'Map generation started',
estimatedTime: response.estimatedTime || '30-90 seconds'
};
} catch (error: any) {
return {
error: error.message,
success: false
};
}
}
/**
* Handle map status check request - uses hybrid architecture
*/
private async handleCheckMapStatus(data: any): Promise<any> {
try {
// SECURITY: Silent GM validation
const gmCheck = this.validateGMAccess();
if (!gmCheck.allowed) {
return { error: 'Access denied', success: false };
}
if (!data.job_id) {
throw new Error('Job ID is required');
}
// Use ComfyUIManager to communicate with backend via WebSocket
const response = await this.comfyuiManager.checkMapStatus(data);
const isSuccess = typeof response?.success === 'boolean' ? response.success : response?.status === 'success';
if (!isSuccess) {
const errorMessage = response?.error || response?.message || 'Status check failed';
return {
error: errorMessage,
success: false,
status: response?.status ?? 'error'
};
}
return {
success: true,
status: response?.status ?? 'success',
job: response.job
};
} catch (error: any) {
return {
error: error.message,
success: false
};
}
}
/**
* Handle map job cancellation request - uses hybrid architecture
*/
private async handleCancelMapJob(data: any): Promise<any> {
try {
// SECURITY: Silent GM validation
const gmCheck = this.validateGMAccess();
if (!gmCheck.allowed) {
return { error: 'Access denied', success: false };
}
if (!data.job_id) {
throw new Error('Job ID is required');
}
// Use ComfyUIManager to communicate with backend via WebSocket
const response = await this.comfyuiManager.cancelMapJob(data);
const isSuccess = typeof response?.success === 'boolean' ? response.success : response?.status === 'success';
if (!isSuccess) {
const errorMessage = response?.error || response?.message || 'Job cancellation failed';
return {
error: errorMessage,
success: false,
status: response?.status ?? 'error'
};
}
return {
success: true,
status: response?.status ?? 'success',
message: response.message || 'Job cancelled successfully'
};
} catch (error: any) {
return {
error: error.message,
success: false
};
}
}
/**
* Handle upload of generated map image (for remote Foundry instances)
* Receives base64-encoded image data and saves it to generated-maps folder
*/
private async handleUploadGeneratedMap(data: any): Promise<any> {
console.log(`[${MODULE_ID}] Upload generated map request received`, {
hasFilename: !!data.filename,
hasImageData: !!data.imageData,
imageDataLength: data.imageData?.length
});
try {
// SECURITY: Silent GM validation
const gmCheck = this.validateGMAccess();
if (!gmCheck.allowed) {
console.error(`[${MODULE_ID}] Upload denied - not GM`);
return { error: 'Access denied', success: false };
}
if (!data.filename || typeof data.filename !== 'string') {
console.error(`[${MODULE_ID}] Upload failed - invalid filename`);
throw new Error('Filename is required and must be a string');
}
if (!data.imageData || typeof data.imageData !== 'string') {
console.error(`[${MODULE_ID}] Upload failed - invalid image data`);
throw new Error('Image data is required and must be a base64 string');
}
console.log(`[${MODULE_ID}] Validating filename...`);
// Validate filename for security (prevent path traversal)
const safeFilename = data.filename.replace(/[^a-zA-Z0-9_\-\.]/g, '_');
if (!safeFilename.endsWith('.png') && !safeFilename.endsWith('.jpg') && !safeFilename.endsWith('.jpeg')) {
throw new Error('Only PNG and JPEG images are supported');
}
console.log(`[${MODULE_ID}] Converting base64 to blob...`, {
base64Length: data.imageData.length,
estimatedSizeMB: (data.imageData.length / 1024 / 1024).toFixed(2)
});
// Convert base64 to Blob
const byteCharacters = atob(data.imageData);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray], { type: 'image/png' });
console.log(`[${MODULE_ID}] Creating file object...`, {
filename: safeFilename,
blobSize: blob.size
});
// Create a File object from the Blob
const file = new File([blob], safeFilename, { type: 'image/png' });
console.log(`[${MODULE_ID}] Ensuring upload directory exists...`);
// Upload to world-specific folder so maps persist even if module is deleted
// This also keeps maps organized per world
const worldId = (game as any).world?.id || 'unknown-world';
const uploadPath = `worlds/${worldId}/ai-generated-maps`;
try {
// Use the modern Foundry API (v13+) with fallback for older versions
const FilePickerAPI = (globalThis as any).foundry?.applications?.apps?.FilePicker?.implementation || (globalThis as any).FilePicker;
await FilePickerAPI.createDirectory('data', uploadPath, { bucket: null });
console.log(`[${MODULE_ID}] Directory created/verified: ${uploadPath}`);
} catch (dirError: any) {
// Directory might already exist, that's okay
if (!dirError.message?.includes('EEXIST') && !dirError.message?.includes('already exists')) {
console.warn(`[${MODULE_ID}] Directory creation warning:`, dirError.message);
}
}
console.log(`[${MODULE_ID}] Uploading to FilePicker...`);
// Upload using Foundry's FilePicker.upload method with modern API
const FilePickerAPI = (globalThis as any).foundry?.applications?.apps?.FilePicker?.implementation || (globalThis as any).FilePicker;
const response = await FilePickerAPI.upload(
'data',
uploadPath,
file,
{},
{ notify: false }
);
console.log(`[${MODULE_ID}] FilePicker.upload response:`, JSON.stringify(response, null, 2));
console.log(`[${MODULE_ID}] Response keys:`, Object.keys(response || {}));
console.log(`[${MODULE_ID}] Uploaded generated map to:`, response.path);
return {
success: true,
path: response.path,
filename: safeFilename,
message: `Map uploaded successfully to ${response.path}`
};
} catch (error: any) {
console.error(`[${MODULE_ID}] Failed to upload generated map:`, error);
return {
error: error.message || 'Failed to upload generated map',
success: false
};
}
}
/**
* Handle move token request
*/
private async handleMoveToken(data: any): Promise<any> {
try {
// SECURITY: Silent GM validation
const gmCheck = this.validateGMAccess();
if (!gmCheck.allowed) {
return { error: 'Access denied', success: false };
}
this.dataAccess.validateFoundryState();
return await this.dataAccess.moveToken(data.tokenId, data.x, data.y, data.animate);
} catch (error) {
throw new Error(`Failed to move token: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Handle update token request
*/
private async handleUpdateToken(data: any): Promise<any> {
try {
// SECURITY: Silent GM validation
const gmCheck = this.validateGMAccess();
if (!gmCheck.allowed) {
return { error: 'Access denied', success: false };
}
this.dataAccess.validateFoundryState();
return await this.dataAccess.updateToken(data.tokenId, data.updates);
} catch (error) {
throw new Error(`Failed to update token: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Handle delete tokens request
*/
private async handleDeleteTokens(data: any): Promise<any> {
try {
// SECURITY: Silent GM validation
const gmCheck = this.validateGMAccess();
if (!gmCheck.allowed) {
return { error: 'Access denied', success: false };
}
this.dataAccess.validateFoundryState();
return await this.dataAccess.deleteTokens(data.tokenIds);
} catch (error) {
throw new Error(`Failed to delete tokens: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Handle get token details request
*/
private async handleGetTokenDetails(data: any): Promise<any> {
try {
// SECURITY: Silent GM validation
const gmCheck = this.validateGMAccess();
if (!gmCheck.allowed) {
return { error: 'Access denied', success: false };
}
this.dataAccess.validateFoundryState();
return await this.dataAccess.getTokenDetails(data.tokenId);
} catch (error) {
throw new Error(`Failed to get token details: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Handle toggle token condition request
*/
private async handleToggleTokenCondition(data: any): Promise<any> {
try {
// SECURITY: Silent GM validation
const gmCheck = this.validateGMAccess();
if (!gmCheck.allowed) {
return { error: 'Access denied', success: false };
}
this.dataAccess.validateFoundryState();
return await this.dataAccess.toggleTokenCondition(data.tokenId, data.conditionId, data.active);
} catch (error) {
throw new Error(`Failed to toggle token condition: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Handle get available conditions request
*/
private async handleGetAvailableConditions(_data: any): Promise<any> {
try {
// SECURITY: Silent GM validation
const gmCheck = this.validateGMAccess();
if (!gmCheck.allowed) {
return { error: 'Access denied', success: false };
}
this.dataAccess.validateFoundryState();
return await this.dataAccess.getAvailableConditions();
} catch (error) {
throw new Error(`Failed to get available conditions: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
}