// _opt/schangar.js import { SlashCommandBuilder } from 'discord.js'; // Export commands array for the centralized handler export const commands = [ { data: new SlashCommandBuilder() .setName('hangarsync') .setDescription('Mark the moment all five lights turn green, for use with hangarstatus') .addStringOption(option => option.setName('timestamp') .setDescription('Custom timestamp (Unix time in seconds or ISO 8601 format). Leave empty for current time.') .setRequired(false)), execute: async (interaction, client) => { const customTimestamp = interaction.options.getString('timestamp'); let syncEpoch; // Attempt to validate custom timestamp if (customTimestamp) { try { if (/^\d+$/.test(customTimestamp)) { const timestampInSeconds = parseInt(customTimestamp); if (timestampInSeconds < 0 || timestampInSeconds > Math.floor(Date.now() / 1000)) { return interaction.reply({ content: 'Invalid timestamp. Please provide a Unix time in seconds that is not in the future.', ephemeral: true }); } syncEpoch = timestampInSeconds * 1000; } else { const date = new Date(customTimestamp); syncEpoch = date.getTime(); if (isNaN(syncEpoch) || syncEpoch < 0) { return interaction.reply({ content: 'Invalid timestamp format. Please use Unix time in seconds or a valid ISO 8601 string.', ephemeral: true }); } } } catch (error) { client.logger.error(`Failed to parse timestamp in hangarsync command: ${error.message}`); return interaction.reply({ content: 'Failed to parse timestamp. Please use Unix time in seconds or a valid ISO 8601 string.', ephemeral: true }); } } else { syncEpoch = Date.now(); } // Check PocketBase connection status if (!isPocketBaseConnected(client)) { client.logger.error('PocketBase not connected when executing hangarsync command'); // Try to reconnect if available if (typeof client.pb.ensureConnection === 'function') { await client.pb.ensureConnection(); // Check if reconnection worked if (!isPocketBaseConnected(client)) { return interaction.reply({ content: 'Database connection unavailable. Please try again later.', ephemeral: true }); } } else { return interaction.reply({ content: 'Database connection unavailable. Please try again later.', ephemeral: true }); } } // Create or update timestamp for guild try { let record = null; try { // First try the enhanced method if available if (typeof client.pb.getFirst === 'function') { record = await client.pb.getFirst('command_hangarsync', `guildId = "${interaction.guildId}"`); } else { // Fall back to standard PocketBase method const records = await client.pb.collection('command_hangarsync').getList(1, 1, { filter: `guildId = "${interaction.guildId}"` }); if (records.items.length > 0) { record = records.items[0]; } } } catch (error) { // Handle case where collection might not exist client.logger.warn(`Error retrieving hangarsync record: ${error.message}`); } if (record) { // Update existing record if (typeof client.pb.updateOne === 'function') { await client.pb.updateOne('command_hangarsync', record.id, { userId: `${interaction.user.id}`, epoch: `${syncEpoch}`, }); } else { await client.pb.collection('command_hangarsync').update(record.id, { userId: `${interaction.user.id}`, epoch: `${syncEpoch}`, }); } client.logger.info(`Updated hangarsync for guild ${interaction.guildId} by user ${interaction.user.id}`); } else { // Create new record if (typeof client.pb.createOne === 'function') { await client.pb.createOne('command_hangarsync', { guildId: `${interaction.guildId}`, userId: `${interaction.user.id}`, epoch: `${syncEpoch}`, }); } else { await client.pb.collection('command_hangarsync').create({ guildId: `${interaction.guildId}`, userId: `${interaction.user.id}`, epoch: `${syncEpoch}`, }); } client.logger.info(`Created new hangarsync for guild ${interaction.guildId} by user ${interaction.user.id}`); } await interaction.reply(`Executive hangar status has been synced: `); } catch (error) { client.logger.error(`Error in hangarsync command: ${error.message}`); await interaction.reply({ content: `Error syncing hangar status. Please try again later.`, ephemeral: true }); } } }, { data: new SlashCommandBuilder() .setName('hangarstatus') .setDescription('Check the status of contested zone executive hangars') .addBooleanOption(option => option.setName('verbose') .setDescription('Extra output, mainly for debugging.') .setRequired(false)), execute: async (interaction, client) => { const verbose = interaction.options.getBoolean('verbose'); // Check PocketBase connection status if (!isPocketBaseConnected(client)) { client.logger.error('PocketBase not connected when executing hangarstatus command'); // Try to reconnect if available if (typeof client.pb.ensureConnection === 'function') { await client.pb.ensureConnection(); // Check if reconnection worked if (!isPocketBaseConnected(client)) { return interaction.reply({ content: 'Database connection unavailable. Please try again later.', ephemeral: true }); } } else { return interaction.reply({ content: 'Database connection unavailable. Please try again later.', ephemeral: true }); } } try { // Get hangarsync data for guild let hangarSync = null; try { // First try the enhanced method if available if (typeof client.pb.getFirst === 'function') { hangarSync = await client.pb.getFirst('command_hangarsync', `guildId = "${interaction.guildId}"`); } else { // Fall back to standard PocketBase methods try { hangarSync = await client.pb.collection('command_hangarsync').getFirstListItem(`guildId = "${interaction.guildId}"`); } catch (error) { // getFirstListItem throws if no items found if (error.status !== 404) throw error; } } if (!hangarSync) { client.logger.info(`No sync data found for guild ${interaction.guildId}`); return interaction.reply({ content: 'No sync data found. Please use `/hangarsync` first to establish a reference point.', ephemeral: true }); } } catch (error) { client.logger.info(`Error retrieving sync data for guild ${interaction.guildId}: ${error.message}`); return interaction.reply({ content: 'No sync data found. Please use `/hangarsync` first to establish a reference point.', ephemeral: true }); } const currentTime = Date.now(); // 5 minutes (all off) + 5*24 minutes (turning green) + 5*12 minutes (turning off) = 185 minutes const cycleDuration = 5 + (5 * 24) + (5 * 12); // Key positions in the cycle const allOffDuration = 5; const turningGreenDuration = 5 * 24; const turningOffDuration = 5 * 12; // Calculate how much time has passed since the epoch const timeSinceEpoch = (currentTime - hangarSync.epoch) / (60 * 1000); // Calculate where we are in the full-cycle relative to the epoch const cyclePosition = ((timeSinceEpoch % cycleDuration) + cycleDuration) % cycleDuration; // Initialize stuff and things const lights = [":black_circle:", ":black_circle:", ":black_circle:", ":black_circle:", ":black_circle:"]; let minutesUntilNextPhase = 0; let currentPhase = ""; // If the epoch is now, we should be at the all-green position. // From there, we need to determine where we are in the cycle. // Case 1: We're in the unlocked phase, right after epoch if (cyclePosition < turningOffDuration) { currentPhase = "Unlocked"; // All lights start as green lights.fill(":green_circle:"); // Calculate how many lights have turned off const offLights = Math.floor(cyclePosition / 12); // Set the appropriate number of lights to off for (let i = 0; i < offLights; i++) { lights[i] = ":black_circle:"; } // Calculate time until next light turns off const timeUntilNextLight = 12 - (cyclePosition % 12); minutesUntilNextPhase = timeUntilNextLight; } // Case 2: We're in the reset phase else if (cyclePosition < turningOffDuration + allOffDuration) { currentPhase = "Resetting"; // Lights are initialized "off", so do nothing with them // Calculate time until all lights turn red const timeIntoPhase = cyclePosition - turningOffDuration; minutesUntilNextPhase = allOffDuration - timeIntoPhase; } // Case 3: We're in the locked phase else { currentPhase = "Locked"; // All lights start as red lights.fill(":red_circle:"); // Calculate how many lights have turned green const timeIntoPhase = cyclePosition - (turningOffDuration + allOffDuration); const greenLights = Math.floor(timeIntoPhase / 24); // Set the appropriate number of lights to green for (let i = 0; i < greenLights; i++) { lights[i] = ":green_circle:"; } // Calculate time until next light turns green const timeUntilNextLight = 24 - (timeIntoPhase % 24); minutesUntilNextPhase = timeUntilNextLight; } // Calculate a timestamp for Discord's formatting and reply const expiration = Math.ceil((Date.now() / 1000) + (minutesUntilNextPhase * 60)); await interaction.reply(`### ${lights[0]} ${lights[1]} ${lights[2]} ${lights[3]} ${lights[4]}`); if (verbose) { await interaction.followUp(`- **Phase**: ${currentPhase}\n- **Status Expiration**: \n- **Epoch**: \n- **Sync**: by <@${hangarSync.userId}>`); // Add additional debug info to logs client.logger.debug(`Hangarstatus for guild ${interaction.guildId}: Phase=${currentPhase}, CyclePosition=${cyclePosition}, TimeSinceEpoch=${timeSinceEpoch}`); } } catch (error) { client.logger.error(`Error in hangarstatus command: ${error.message}`); await interaction.reply({ content: `Error retrieving hangar status. Please try again later.`, ephemeral: true }); } } } ]; // Function to check PocketBase connection status function isPocketBaseConnected(client) { // Check multiple possible status indicators to be safe return client.pb && ( // Check status object (original code style) (client.pb.status && client.pb.status.connected) || // Check isConnected property (pbutils module style) client.pb.isConnected === true || // Last resort: check if authStore is valid client.pb.authStore?.isValid === true ); } // Initialize module export const init = async (client, config) => { client.logger.info('Initializing Star Citizen Hangar Status module'); // Check PocketBase connection if (!isPocketBaseConnected(client)) { client.logger.warn('PocketBase not connected at initialization'); // Try to reconnect if available if (typeof client.pb.ensureConnection === 'function') { await client.pb.ensureConnection(); } } else { client.logger.info('PocketBase connection confirmed'); } client.logger.info('Star Citizen Hangar Status module initialized'); };