diff --git a/plugins/minimap/controller.ts b/plugins/minimap/controller.ts index 83445447..73cd7a05 100644 --- a/plugins/minimap/controller.ts +++ b/plugins/minimap/controller.ts @@ -35,7 +35,12 @@ export class ControllerPlugin extends BaseControllerPlugin { private chartTagsPath: string = ""; private chunkSavingQueue = new Map>(); private chartTagQueues = new Map(); - private seenTagNumbers = new Map>(); + private lastSeenTagContent = new Map>(); private savingTiles: boolean = false; private savingChartTags = new Set(); private saveInterval: NodeJS.Timeout | null = null; @@ -61,7 +66,7 @@ export class ControllerPlugin extends BaseControllerPlugin { this.setupTileRoutes(); // Load existing tag numbers for deduplication - await this.loadExistingTagNumbers(); + await this.loadExistingTagContent(); this.saveInterval = setInterval(() => { this.saveTiles().catch(err => this.logger.error(`Error saving tiles: ${err}`)); @@ -377,7 +382,7 @@ export class ControllerPlugin extends BaseControllerPlugin { return Buffer.concat([existingTile, ...appendBuffers]); } - async loadExistingTagNumbers() { + async loadExistingTagContent() { try { if (!await fs.pathExists(this.chartTagsPath)) { return; @@ -390,10 +395,10 @@ export class ControllerPlugin extends BaseControllerPlugin { const fileKey = file.replace('_chart_tags.json', ''); const filePath = path.join(this.chartTagsPath, file); - if (!this.seenTagNumbers.has(fileKey)) { - this.seenTagNumbers.set(fileKey, new Set()); + if (!this.lastSeenTagContent.has(fileKey)) { + this.lastSeenTagContent.set(fileKey, new Map()); } - const seenTags = this.seenTagNumbers.get(fileKey)!; + const seenTags = this.lastSeenTagContent.get(fileKey)!; try { const content = await fs.readFile(filePath, 'utf-8'); @@ -402,21 +407,27 @@ export class ControllerPlugin extends BaseControllerPlugin { for (const line of lines) { try { const tagData = JSON.parse(line); - if (typeof tagData.tag_number === 'number') { - seenTags.add(tagData.tag_number); + if (typeof tagData.tag_number === 'number' && !tagData.end_tick) { + // Only store the latest content for tags that haven't been deleted + seenTags.set(tagData.tag_number, { + position: tagData.position, + text: tagData.text, + icon: tagData.icon, + last_user: tagData.last_user + }); } } catch (parseErr) { this.logger.warn(`Failed to parse chart tag line in ${file}: ${parseErr}`); } } - this.logger.info(`Loaded ${seenTags.size} existing tag numbers from ${file}`); + this.logger.info(`Loaded ${seenTags.size} existing tag contents from ${file}`); } catch (readErr) { this.logger.warn(`Failed to read chart tag file ${file}: ${readErr}`); } } } catch (err) { - this.logger.error(`Error loading existing tag numbers: ${err}`); + this.logger.error(`Error loading existing tag content: ${err}`); } } @@ -449,12 +460,17 @@ export class ControllerPlugin extends BaseControllerPlugin { await fs.appendFile(filePath, jsonLines + '\n'); // Mark tags as successfully saved - if (!this.seenTagNumbers.has(fileKey)) { - this.seenTagNumbers.set(fileKey, new Set()); + if (!this.lastSeenTagContent.has(fileKey)) { + this.lastSeenTagContent.set(fileKey, new Map()); } - const seenTags = this.seenTagNumbers.get(fileKey)!; + const seenTags = this.lastSeenTagContent.get(fileKey)!; for (const tag of tagsToSave) { - seenTags.add(tag.tag_number); + seenTags.set(tag.tag_number, { + position: tag.position, + text: tag.text, + icon: tag.icon, + last_user: tag.last_user + }); } this.logger.info(`Saved ${tagsToSave.length} chart tag entries to ${fileKey}`); @@ -479,15 +495,30 @@ export class ControllerPlugin extends BaseControllerPlugin { // Create file key based on instance, surface, and force const fileKey = `${instance_id}_${tag_data.surface}_${tag_data.force}`; - // Initialize seen tags set if it doesn't exist - if (!this.seenTagNumbers.has(fileKey)) { - this.seenTagNumbers.set(fileKey, new Set()); + // Initialize seen tags map if it doesn't exist + if (!this.lastSeenTagContent.has(fileKey)) { + this.lastSeenTagContent.set(fileKey, new Map()); } - // Check for duplicate tag_number - const seenTags = this.seenTagNumbers.get(fileKey)!; - if (seenTags.has(tag_data.tag_number)) { - return; + // Check for duplicate content, but allow deletion events (end_tick set) even if we've seen the tag before + const seenTags = this.lastSeenTagContent.get(fileKey)!; + const lastSeenContent = seenTags.get(tag_data.tag_number); + + // For deletion events (end_tick set), always allow through + if (tag_data.end_tick !== undefined) { + // Don't skip deletion events + } else if (lastSeenContent) { + // For creation/modification events, check if content has actually changed + const contentUnchanged = + lastSeenContent.position[0] === tag_data.position[0] && + lastSeenContent.position[1] === tag_data.position[1] && + lastSeenContent.text === tag_data.text && + JSON.stringify(lastSeenContent.icon) === JSON.stringify(tag_data.icon) && + lastSeenContent.last_user === tag_data.last_user; + + if (contentUnchanged) { + return; // Skip if content hasn't changed + } } // Add instance_id to the tag data for storage diff --git a/plugins/minimap/messages.ts b/plugins/minimap/messages.ts index fec8dcff..e427c8b5 100644 --- a/plugins/minimap/messages.ts +++ b/plugins/minimap/messages.ts @@ -28,8 +28,8 @@ export const ChartTagDataSchema = Type.Object({ surface: Type.String(), position: Type.Tuple([Type.Number(), Type.Number()]), text: Type.String(), - icon: Type.Union([SignalIDSchema, Type.Null()]), - last_user: Type.Union([Type.String(), Type.Null()]), + icon: Type.Optional(SignalIDSchema), + last_user: Type.Optional(Type.String()), }); export type ChartTagData = Static; diff --git a/plugins/minimap/module/minimap.lua b/plugins/minimap/module/minimap.lua index 4dcb301c..5c3b4c90 100644 --- a/plugins/minimap/module/minimap.lua +++ b/plugins/minimap/module/minimap.lua @@ -102,13 +102,13 @@ local function on_chart_tag_modified(event) return end - -- End the old version + -- End the old version at current tick local end_tick = game.tick local old_tag_data = create_tag_data(tag, nil, end_tick) send_chart_tag_data(old_tag_data) - -- Create new tag entry - local start_tick = game.tick + -- Create new tag entry starting at next tick + local start_tick = game.tick + 1 local tag_data = create_tag_data(tag, start_tick, nil) -- Send to plugin @@ -121,7 +121,7 @@ local function on_chart_tag_removed(event) end local tag = event.tag - if not tag then + if not tag or not tag.valid then return end diff --git a/plugins/minimap/web/CanvasMinimapPage.tsx b/plugins/minimap/web/CanvasMinimapPage.tsx index 593c3466..e3941589 100644 --- a/plugins/minimap/web/CanvasMinimapPage.tsx +++ b/plugins/minimap/web/CanvasMinimapPage.tsx @@ -27,8 +27,8 @@ interface MergedChartTag { surface: string; position: [number, number]; text: string; - icon: SignalID | null; - last_user: string | null; + icon?: SignalID; + last_user?: string; instance_id: number; } @@ -107,6 +107,7 @@ export default function CanvasMinimapPage() { const [currentTick, setCurrentTick] = useState(null); const [isPlaying, setIsPlaying] = useState(false); const [playbackSpeed, setPlaybackSpeed] = useState(1); + const [chartTagTimestamps, setChartTagTimestamps] = useState(0); // Chart tag states const [showChartTags, setShowChartTags] = useState(true); @@ -156,6 +157,7 @@ export default function CanvasMinimapPage() { availableTicks: [] as number[], showChartTags: true, mergedChartTags: [] as MergedChartTag[], + chartTagTimestamps: 0, }); const control = useContext(ControlContext); @@ -227,8 +229,9 @@ export default function CanvasMinimapPage() { availableTicks, showChartTags, mergedChartTags, + chartTagTimestamps, }; - }, [selectedInstance, selectedSurface, selectedForce, isTimelapseMode, currentTick, availableTicks, showChartTags, mergedChartTags]); + }, [selectedInstance, selectedSurface, selectedForce, isTimelapseMode, currentTick, availableTicks, showChartTags, mergedChartTags, chartTagTimestamps]); // Define pixel-perfect zoom levels to eliminate seaming issues const ZOOM_LEVELS = [0.125, 0.25, 0.5, 0.75, 1, 1.25, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10]; @@ -363,6 +366,30 @@ export default function CanvasMinimapPage() { } } + // Also include chart tag timestamps + const currentState = currentStateRef.current; + let tagsWithTicks = 0; + + if (currentState.mergedChartTags.length > 0) { + for (const tag of currentState.mergedChartTags) { + // Add start_tick if it exists (tag creation) + if (tag.start_tick !== undefined) { + allTicks.add(tag.start_tick); + tagsWithTicks++; + } + + // Add end_tick if it exists (tag deletion) + if (tag.end_tick !== undefined) { + allTicks.add(tag.end_tick); + tagsWithTicks++; + } + } + + console.log(`Added ${tagsWithTicks} chart tag timestamps to timeline`); + } + + setChartTagTimestamps(tagsWithTicks); + const sortedTicks = Array.from(allTicks).sort((a, b) => a - b); setAvailableTicks(sortedTicks); @@ -380,9 +407,14 @@ export default function CanvasMinimapPage() { setIsPlaying(false); if (enabled) { + // Ensure chart tags are loaded before getting available ticks + if (selectedInstance && selectedSurface && selectedForce) { + await loadExistingChartTags(); + } await loadAvailableTicks(); } else { setCurrentTick(null); + setChartTagTimestamps(0); } // Clear caches when switching modes @@ -1283,8 +1315,10 @@ export default function CanvasMinimapPage() { useEffect(() => { if (isTimelapseMode && selectedInstance && selectedSurface && selectedForce) { // Debounce the reload to avoid too many requests during navigation - const timeoutId = setTimeout(() => { - loadAvailableTicks(); + const timeoutId = setTimeout(async () => { + // Ensure chart tags are loaded before getting available ticks + await loadExistingChartTags(); + await loadAvailableTicks(); }, 500); return () => clearTimeout(timeoutId); @@ -1332,8 +1366,9 @@ export default function CanvasMinimapPage() { return false; } - // In timelapse mode, filter by current tick + // Filter by tick range (both in timelapse and live mode) if (currentState.isTimelapseMode && currentState.currentTick !== null) { + // In timelapse mode, filter by current tick // Tag is visible if it started before or at current tick and hasn't ended yet if (tag.start_tick !== undefined && tag.start_tick > currentState.currentTick) { return false; @@ -1341,6 +1376,11 @@ export default function CanvasMinimapPage() { if (tag.end_tick !== undefined && tag.end_tick <= currentState.currentTick) { return false; } + } else { + // In live mode, only hide tags that have been deleted (have an end_tick) + if (tag.end_tick !== undefined) { + return false; + } } return true; @@ -1547,6 +1587,11 @@ export default function CanvasMinimapPage() { {currentTick ? formatTickTime(currentTick) : "No data"} +
+ + {availableTicks.length} total timestamps + +