Updated watch and control.lua
This commit is contained in:
14
control.lua
14
control.lua
@@ -1,5 +1,5 @@
|
|||||||
local EXPORTER_NAME = "signal-exporter"
|
local EXPORTER_NAME = "signal-exporter"
|
||||||
local EXPORT_INTERVAL = 18000 -- Ticks between file writes (18000 = 5 minutes)
|
local EXPORT_INTERVAL = 1800 -- Ticks between file writes (1800 = 30s at 60 UPS)
|
||||||
|
|
||||||
local function init_global()
|
local function init_global()
|
||||||
global.exporters = global.exporters or {}
|
global.exporters = global.exporters or {}
|
||||||
@@ -17,6 +17,7 @@ local function export_combinator(unit_number, data)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local signals = {
|
local signals = {
|
||||||
|
game_tick = game.tick,
|
||||||
circuit_network = {
|
circuit_network = {
|
||||||
red = {},
|
red = {},
|
||||||
green = {}
|
green = {}
|
||||||
@@ -165,39 +166,28 @@ script.on_event(defines.events.on_gui_closed, function(event)
|
|||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
||||||
-- Function to export all item prototypes to a CSV file.
|
|
||||||
local function export_item_names_csv(event)
|
local function export_item_names_csv(event)
|
||||||
local player = game.players[event.player_index]
|
local player = game.players[event.player_index]
|
||||||
local csv_lines = { '"item_key","localised_name"' }
|
local csv_lines = { '"item_key","localised_name"' }
|
||||||
|
|
||||||
-- Helper function to safely escape double quotes for CSV format.
|
|
||||||
local function escape_csv_field(value)
|
local function escape_csv_field(value)
|
||||||
if value == nil then return "" end
|
if value == nil then return "" end
|
||||||
return string.gsub(tostring(value), '"', '""')
|
return string.gsub(tostring(value), '"', '""')
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Iterate through all registered item prototypes.
|
|
||||||
for _, item_prototype in pairs(game.item_prototypes) do
|
for _, item_prototype in pairs(game.item_prototypes) do
|
||||||
local key = item_prototype.name
|
local key = item_prototype.name
|
||||||
|
|
||||||
-- Use player.localise() to resolve the LocalisedString object (item_prototype.localised_name)
|
|
||||||
-- into the final, human-readable text for the current player's language.
|
|
||||||
local resolved_name = player.localise(item_prototype.localised_name)
|
local resolved_name = player.localise(item_prototype.localised_name)
|
||||||
|
|
||||||
local escaped_key = escape_csv_field(key)
|
local escaped_key = escape_csv_field(key)
|
||||||
local escaped_name = escape_csv_field(resolved_name)
|
local escaped_name = escape_csv_field(resolved_name)
|
||||||
|
|
||||||
table.insert(csv_lines, '"' .. escaped_key .. '","' .. escaped_name .. '"')
|
table.insert(csv_lines, '"' .. escaped_key .. '","' .. escaped_name .. '"')
|
||||||
end
|
end
|
||||||
|
|
||||||
local csv_content = table.concat(csv_lines, "\n")
|
local csv_content = table.concat(csv_lines, "\n")
|
||||||
game.write_file("item_names.csv", csv_content, false, player.index)
|
game.write_file("item_names.csv", csv_content, false, player.index)
|
||||||
|
|
||||||
player.print("Export complete. File saved to 'script-output/item_names.csv'.")
|
player.print("Export complete. File saved to 'script-output/item_names.csv'.")
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Register a console command to trigger the export.
|
|
||||||
commands.add_command(
|
commands.add_command(
|
||||||
"export-item-names",
|
"export-item-names",
|
||||||
"Exports the internal name (key) and localised name for all game items to a CSV file.",
|
"Exports the internal name (key) and localised name for all game items to a CSV file.",
|
||||||
|
|||||||
106
node/watch.ts
106
node/watch.ts
@@ -3,37 +3,33 @@ import { readdir, readFile, stat } from 'fs/promises';
|
|||||||
import { resolve, basename, extname, join, dirname } from 'path';
|
import { resolve, basename, extname, join, dirname } from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
// --- Configuration ---
|
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
function loadEnv() {
|
function loadEnv(): void {
|
||||||
const envPath = resolve(__dirname, '.env');
|
const envPath = resolve(__dirname, '.env');
|
||||||
try {
|
try {
|
||||||
const envFileContent = readFileSync(envPath, 'utf-8');
|
const content = readFileSync(envPath, 'utf-8');
|
||||||
envFileContent.split(/\r?\n/).forEach(line => {
|
for (const line of content.split(/\r?\n/)) {
|
||||||
const trimmedLine = line.trim();
|
const trimmed = line.trim();
|
||||||
if (trimmedLine && !trimmedLine.startsWith('#')) {
|
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||||
const parts = trimmedLine.split('=', 2);
|
const [key, value] = trimmed.split('=', 2);
|
||||||
if (parts.length === 2) {
|
if (key && value !== undefined) {
|
||||||
const [key, value] = parts;
|
|
||||||
process.env[key.trim()] = value.trim().replace(/^"|"$/g, '');
|
process.env[key.trim()] = value.trim().replace(/^"|"$/g, '');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
console.log('Loaded configuration from .env');
|
||||||
console.log('Successfully loaded configuration from .env file.');
|
} catch {
|
||||||
} catch (error) {
|
console.error('FATAL: Could not read .env file.');
|
||||||
console.error('FATAL: Could not find or read the .env file. Please ensure it exists in the same directory as this script.');
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadEnv();
|
loadEnv();
|
||||||
|
|
||||||
const { WEBHOOK_BASE_URL, BASIC_AUTH_USERNAME, BASIC_AUTH_PASSWORD } = process.env;
|
const { INGEST_BASE_URL, API_TOKEN } = process.env;
|
||||||
|
|
||||||
if (!WEBHOOK_BASE_URL || !BASIC_AUTH_USERNAME || !BASIC_AUTH_PASSWORD) {
|
if (!INGEST_BASE_URL || !API_TOKEN) {
|
||||||
console.error('FATAL: Please define WEBHOOK_BASE_URL, BASIC_AUTH_USERNAME, and BASIC_AUTH_PASSWORD in your .env file.');
|
console.error('FATAL: INGEST_BASE_URL and API_TOKEN must be defined in .env');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,100 +37,92 @@ const WATCH_PATH = resolve(__dirname, '..', '..', '..', 'script-output', 'signal
|
|||||||
const DEBOUNCE_DELAY_MS = 250;
|
const DEBOUNCE_DELAY_MS = 250;
|
||||||
const debounceTimers = new Map<string, NodeJS.Timeout>();
|
const debounceTimers = new Map<string, NodeJS.Timeout>();
|
||||||
|
|
||||||
// --- Core Logic ---
|
async function sendToWebhook(filePath: string, mtimeMs: number): Promise<void> {
|
||||||
|
|
||||||
async function sendToWebhook(filePath: string) {
|
|
||||||
try {
|
|
||||||
const combinatorName = basename(filePath, extname(filePath));
|
const combinatorName = basename(filePath, extname(filePath));
|
||||||
const url = `${WEBHOOK_BASE_URL}/${encodeURIComponent(combinatorName)}`;
|
const url = `${INGEST_BASE_URL}/api/ingest/${encodeURIComponent(combinatorName)}?token=${API_TOKEN}`;
|
||||||
|
|
||||||
|
try {
|
||||||
const content = await readFile(filePath, 'utf-8');
|
const content = await readFile(filePath, 'utf-8');
|
||||||
|
|
||||||
// Silently ignore empty files, which can occur during file writing.
|
|
||||||
if (!content.trim()) return;
|
if (!content.trim()) return;
|
||||||
JSON.parse(content);
|
JSON.parse(content); // validate before sending
|
||||||
|
|
||||||
const authHeader = 'Basic ' + Buffer.from(`${BASIC_AUTH_USERNAME}:${BASIC_AUTH_PASSWORD}`).toString('base64');
|
console.log(`Sending '${combinatorName}' (mtime: ${new Date(mtimeMs).toISOString()})`);
|
||||||
|
|
||||||
console.log(`Sending data for '${combinatorName}'...`);
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'POST', // Using POST to send a request body, as is standard for webhooks.
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': authHeader,
|
'X-File-Mtime': String(mtimeMs),
|
||||||
},
|
},
|
||||||
body: content,
|
body: content,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP Error: ${response.status} ${response.statusText}`);
|
throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Successfully sent data for '${combinatorName}'. Server responded with ${response.status}.`);
|
console.log(`OK '${combinatorName}' → ${response.status}`);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const fileName = basename(filePath);
|
|
||||||
if (err instanceof SyntaxError) {
|
if (err instanceof SyntaxError) {
|
||||||
console.error(`Skipping ${fileName}: Invalid JSON content. The file might be incomplete.`);
|
console.error(`Skip '${combinatorName}': invalid JSON`);
|
||||||
} else {
|
} else {
|
||||||
console.error(`Error sending webhook for ${fileName}:`, err.message);
|
console.error(`Error '${combinatorName}':`, err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFileChange(filename: string | null) {
|
function handleFileChange(filename: string | null): void {
|
||||||
if (!filename || !filename.endsWith('.json')) return;
|
if (!filename?.endsWith('.json')) return;
|
||||||
|
|
||||||
if (debounceTimers.has(filename)) {
|
const existing = debounceTimers.get(filename);
|
||||||
clearTimeout(debounceTimers.get(filename)!);
|
if (existing) clearTimeout(existing);
|
||||||
}
|
|
||||||
|
|
||||||
const newTimer = setTimeout(async () => {
|
const timer = setTimeout(async () => {
|
||||||
const fullPath = join(WATCH_PATH, filename);
|
const fullPath = join(WATCH_PATH, filename);
|
||||||
try {
|
try {
|
||||||
const stats = await stat(fullPath);
|
const stats = await stat(fullPath);
|
||||||
if (stats.isFile()) {
|
if (stats.isFile()) {
|
||||||
await sendToWebhook(fullPath);
|
await sendToWebhook(fullPath, stats.mtimeMs);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Error (e.g., file deleted before processing), ignore.
|
// file removed before processing — ignore
|
||||||
} finally {
|
} finally {
|
||||||
debounceTimers.delete(filename);
|
debounceTimers.delete(filename);
|
||||||
}
|
}
|
||||||
}, DEBOUNCE_DELAY_MS);
|
}, DEBOUNCE_DELAY_MS);
|
||||||
|
|
||||||
debounceTimers.set(filename, newTimer);
|
debounceTimers.set(filename, timer);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initialScanAndProcess() {
|
async function initialScan(): Promise<void> {
|
||||||
console.log(`Performing initial scan of directory: ${WATCH_PATH}`);
|
console.log(`Initial scan: ${WATCH_PATH}`);
|
||||||
try {
|
try {
|
||||||
const files = await readdir(WATCH_PATH);
|
const files = (await readdir(WATCH_PATH)).filter(f => f.endsWith('.json'));
|
||||||
const jsonFiles = files.filter(f => f.endsWith('.json'));
|
console.log(`Found ${files.length} file(s)`);
|
||||||
console.log(`Found ${jsonFiles.length} existing JSON file(s). Processing...`);
|
for (const file of files) {
|
||||||
for (const file of jsonFiles) {
|
const fullPath = join(WATCH_PATH, file);
|
||||||
await sendToWebhook(join(WATCH_PATH, file));
|
const stats = await stat(fullPath);
|
||||||
|
await sendToWebhook(fullPath, stats.mtimeMs);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(`Error during initial scan: ${err.message}. Ensure the directory exists.`);
|
console.error(`Initial scan failed: ${err.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Script Execution ---
|
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
await initialScanAndProcess();
|
await initialScan();
|
||||||
|
|
||||||
console.log(`\nWatching for file changes in: ${WATCH_PATH}`);
|
console.log(`Watching: ${WATCH_PATH}`);
|
||||||
try {
|
try {
|
||||||
watch(WATCH_PATH, (eventType, filename) => {
|
watch(WATCH_PATH, (eventType, filename) => {
|
||||||
if (filename) {
|
if (filename) {
|
||||||
console.log(`[${new Date().toLocaleTimeString()}] Event '${eventType}' on '${filename}'`);
|
console.log(`[${new Date().toLocaleTimeString()}] ${eventType}: ${filename}`);
|
||||||
handleFileChange(filename);
|
handleFileChange(filename);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
console.log("Watcher is now running. Press Ctrl+C to stop.");
|
console.log('Watcher running. Ctrl+C to stop.');
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(`FATAL: Failed to start watcher. Please check if the path is correct. Error: ${err.message}`);
|
console.error(`FATAL: Watcher failed: ${err.message}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
Reference in New Issue
Block a user