mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 23:04:52 +09:00
Fix deleting and modifying tags, tags on timeline
This commit is contained in:
1 parent
fc466ec799
commit
cf2efb1380
4 files changed
+109
-33
No files matched your search
@@ -35,7 +35,12 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
private chartTagsPath: string = "";
|
||||
private chunkSavingQueue = new Map<string, Map<string, { data: ChartData, tick: number }>>();
|
||||
private chartTagQueues = new Map<string, ChartTagData[]>();
|
||||
private seenTagNumbers = new Map<string, Set<number>>();
|
||||
private lastSeenTagContent = new Map<string, Map<number, {
|
||||
position: [number, number];
|
||||
text: string;
|
||||
icon?: any;
|
||||
last_user?: string;
|
||||
}>>();
|
||||
private savingTiles: boolean = false;
|
||||
private savingChartTags = new Set<string>();
|
||||
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<number>());
|
||||
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<number>());
|
||||
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<number>());
|
||||
// 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
|
||||
|
||||
@@ -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<typeof ChartTagDataSchema>;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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<number | null>(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() {
|
||||
<Text>
|
||||
{currentTick ? formatTickTime(currentTick) : "No data"}
|
||||
</Text>
|
||||
<div style={{ marginTop: "4px", fontSize: "12px", color: "#666" }}>
|
||||
<Text type="secondary">
|
||||
{availableTicks.length} total timestamps
|
||||
</Text>
|
||||
</div>
|
||||
<Slider
|
||||
min={0}
|
||||
max={availableTicks.length - 1}
|
||||
|
||||
Reference in new issue
Block a user