A cross-platform desktop application for browsing and editing Kerbal Space Program save files (.sfs), craft files (.craft), and configuration files (.cfg).
Existing save editors are either Windows-only, or abandoned years ago, or limited to specific tasks like editing ships. KML (Kerbal Markup Lister) — the most recent .NET option — can be run on Linux via the terminal only, with no GUI. SFS Analyzer aims to be a modern alternative that runs on both Linux and Windows without compromises.
Click the toolbar button to select a file. The same action can be performed with Ctrl+O. On Linux the KSP installation directory is often a hidden folder (~/.steam/...). Press Ctrl+H in the file dialog to show hidden directories.
Writes changes back to the file. Before every save a .bakN copy (next free number) is created automatically in the same directory, so earlier versions are never lost. The same action can be performed with Ctrl+S.
Hierarchical tree of all nodes in the file. Select any node to inspect and edit its parameters in the right panel. Nodes can be expanded and collapsed to navigate the structure.
Copies the selected node to the system clipboard as SFS source text. The node (including all parameters, comments, and child nodes) is serialised into standard SFS format, so it can be pasted into any text editor or into another instance of SFS Analyzer. Ctrl+C copies the selected node.
Reads the system clipboard and parses the content as an SFS node definition. Click Paste to insert as a child of the selected node. Shift+click Paste inserts as a sibling after the copied source node, grouped by type name. Keyboard shortcuts: Ctrl+V pastes as child, Ctrl+Shift+V pastes as sibling. When pasting as sibling, if the node has a parent parameter referencing a sibling index, its value is recalculated for the new context (matching the behaviour of Clone Node).
Paste works with any valid single-node SFS text — nodes copied from SFS Analyzer, typed manually, or copied from a .craft/.sfs file.
Copy/Paste is well suited for cloning launched vessels or moving vessels between save files. When transferring a crewed vessel, either also transfer the relevant kerbals from the ROSTER section, or remove the crew parameters from all inhabited parts (PART nodes) before loading the vessel — otherwise the game will hang.
Reorders a sibling node within its parent. Click Move Up or Move Down to shift by one position. Shift+click moves by five. Ctrl+click moves to the top or bottom. Keyboard shortcuts: Alt+Up / Alt+Down (one), Alt+Shift+Up / Alt+Shift+Down (five), Alt+Home / Alt+End (top/bottom).
Creates a deep copy of the selected node and inserts it as a sibling in the same parent. The cloned subtree includes all parameters and child nodes. If the source has a parent parameter referencing a sibling index, the clone's parent is updated to point to the original node's index.
Creates a new empty child node under the selected node. The node name is provided in a dialog. The new node is immediately added to the tree and expanded for visibility.
Removes the selected node and its entire subtree. A confirmation dialog prevents accidental deletion.
Adds a new key-value pair to the selected node. A dialog prompts for the key and value. The new entry is appended at the end of the node's entry list and appears immediately in the parameters panel.
The right panel shows all key-value pairs of the selected node. Values are edited inline in a text field — press Enter or focus away to confirm the change. Entries are displayed in file order, preserving the original layout.
At the top of the panel, a breadcrumb path shows the full ancestor chain of the selected node. Intermediate nodes are clickable — clicking one navigates the tree to that node.
Nodes with a parent parameter (integer index referencing a sibling of the same type) show a jump button in the top-left corner of the parameters panel, next to the breadcrumbs. Clicking it selects and scrolls to the referenced node.
Existing node parameters can be renamed or deleted.
RGB triples ending in *R, *G, *B with float values in [0, 1] are automatically grouped into a single row with a ColorPicker. Changes update all three fields simultaneously.
Full-text search across all node names and parameter values. Results are highlighted in the tree, and the status bar shows the current match number. Arrow buttons cycle through matches.
The bottom bar shows the current file name, node count, and file size after loading. During search it displays match progress; after saving it confirms the operation.
The toolbar's cog icon opens the settings dialog with several tabs (built-in tabs plus any added by plugins). The same action can be performed with Ctrl+P.
Controls core application settings:
- Indent mode — Tab or Spaces (when Spaces is selected, indent width can be adjusted from 1 to 8)
- Create backup before saving — when checked, a
.bakNcopy (next free number) is created before every save - GameData path — path to the KSP GameData directory, used by plugins to load part configs and other game assets. Click Browse… to select a directory
- Theme — UI theme; select a theme and click OK to apply.
Choose from several built-in themes via the Theme dropdown on the General tab. Themes are implemented with AtlantaFX and include:
- Default — JavaFX Modena (the standard look)
- Cupertino Light / Cupertino Dark — macOS-inspired appearance
- Dracula — a dark theme with a purple tint
- Nord Light / Nord Dark — based on the Nord color palette
- Primer Light / Primer Dark — GitHub-style design
Select a theme from the dropdown and click OK to apply it. The choice is persisted and restored on next launch.
Configures how nodes are displayed in the tree view. The default parameter (default: name) is the key whose value is shown alongside the node name — e.g. a VESSEL node with name = My Ship renders as VESSEL: My Ship. Per-type overrides can set a different default parameter and extra parameters for specific paths; extra values appear as key = value after a comma.
Lists all discovered plugins with their metadata (name, author, version). Each plugin can be enabled or disabled via a checkbox — changes take effect after restart. Disabled plugins are never loaded into the JVM.
Lists all kerbals from the loaded save file's ROSTER section in a detailed table.
- 12 columns: Name, Gender (♂/♀), Type, Trait, Courage/Stupidity (progress bars), Badass/Veteran/Hero (checkmarks), State, Suit, Vessel.
- Bidirectional selection: click a name => select KERBAL node; click a vessel name => select VESSEL node; select a KERBAL/VESSEL node in the tree => highlight the matching row.
- Vessel column shows the name of the vessel the kerbal is currently assigned to.
Lists all vessels from the loaded save file in a table view.
-
Name column always visible; Type, Situation, SOI, On Board, and crew breakdown columns toggleable via Settings.
-
Crew breakdown: separate columns for Pilots, Engineers, Scientists, Tourists — with per-kerbal tooltips.
-
Context menu on crew count cells — right-click a number to jump to the corresponding kerbal node in the ROSTER tree.
-
Bidirectional selection: click a table row to select the VESSEL node in the tree; select a VESSEL in the tree => highlight the matching row.
-
SOI resolved from ORBIT/REF using stock celestial body indices.
-
Settings page under Plugins => Vessel List.
Exports a launched vessel from a save file as a .craft file loadable in the VAB/SPH.
- Selection-driven: the toolbar button is enabled only when a VESSEL node is selected.
- Part tree reconstruction with parent-relative position/rotation transforms.
- Symmetry handling: save-file index references converted to part-name references.
- Surface attachment (
srfN) resolution from index-based to part-name-based format. - Node stack geometry discovered dynamically by scanning
GameData/*.cfgfiles fornode_stack_*definitions. - Deterministic DFS output order for reproducible exports.
⚠️ GameData path: for correct node-stack discovery the path to the KSP installation'sGameData/directory must be set in File => Settings => General tab ("GameData path:" field). Without it, part attachment nodes are resolved by name only, which may produce incorrect.craftoutput on parts with non-standard attach-node layouts.⚠️ Known limitation: may produce incorrect.craftoutput on complex vessels, especially those with docking-node-attached subassemblies. Work in progress.
Detects and fixes stuck docking ports in the save file.
- Scan-driven: click the toolbar button to scan all vessels for docking port pairs.
- Stuck detection: flags pairs where
ModuleDockingNode.statedeviates fromDocked (dockee)/Docked (docker). - Per-pair Fix button and Fix All Stuck button — rewrites state to the correct values.
- Auto-refresh: re-scans on document change (debounced 500ms) when the tab is open.
- Vessel column with hyperlink navigation.
- Settings page with "Show OK ports" toggle (default on).
- Sort order preserved across rescans.
Pass a file path as the first argument to open it directly on startup:
java -jar sfsanalyzer-app-<version>.jar path/to/quicksave.sfs*.sfs— KSP save files*.cfg— KSP configuration files*.craft— KSP craft files
-
Make sure you have Java 21 (or later) installed (a JRE is sufficient for running).
-
Download the latest release ZIP and extract it to a directory of your choice.
-
Run:
java -jar sfsanalyzer-app-<version>.jar
For convenience, create a launcher:
Windows — a shortcut with Target set to
javaw.exe -jar sfsanalyzer-app-<version>.jar, Start in set to the installation directory, and Change Icon... pointing tosfsanalyzer.ico(included in the release ZIP).Linux — a
.desktopfile referencing the JAR andicon.pngfrom the release ZIP:[Desktop Entry] Type=Application Name=SFS Analyzer Exec=java -jar sfsanalyzer-app-<version>.jar Path=/path/to/installation Icon=/path/to/icon.png Terminal=false
Place the file at
~/.local/share/applications/sfsanalyzer.desktopfor the current user or/usr/share/applications/system-wide.
- Java 21 JDK
- Apache Maven 3.8+
mvn clean packageProduces:
app/target/sfsanalyzer-app-<version>.jar— runnable application (fat JAR)api/target/sfsanalyzer-plugin-api-<version>.jar— plugin API librarydemo-plugins/*/target/sfsanalyzer-demo-*.jar— demo pluginsplugins/*/target/*.jar— bundled plugins (see Bundled Plugins)
Build produces app/target/sfsanalyzer-app-<version>.jar. To run:
java -jar app/target/sfsanalyzer-app-<version>.jarOptionally create a plugins/ directory next to the JAR to enable the plugin system.
Plugins are discovered by scanning JARs in the plugins/ directory; disabled plugins are never loaded into the JVM.
If you plan to use an AI assistant to write plugins, see PLUGIN_AGENTS.md. It covers the full API guide, model classes, project structure, build setup, and deployment. To make an AI assistant pick up the instructions automatically, rename PLUGIN_AGENTS.md to the filename your assistant expects (e.g. AGENTS.md or CLAUDE.md) and place it in your plugin project root.
-
Add the API as a dependency:
<dependency> <groupId>io.github.crvx</groupId> <artifactId>sfsanalyzer-plugin-api</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
-
Implement
SfsAnalyzerPlugin:package com.example.myplugin; import io.github.crvx.sfsanalyzer.api.*; public class MyPlugin implements SfsAnalyzerPlugin { @Override public String getName() { return "My Plugin"; } @Override public void init(PluginContext ctx) { ToolbarButton btn = ctx.addToolbarButton( "Do Something", "fas-star", (node, mods) -> System.out.println("Selected: " + node.getName())); btn.setEnabled(false); ctx.addSelectionListener(n -> btn.setEnabled(n != null)); } }
-
Create a service descriptor at
META-INF/services/io.github.crvx.sfsanalyzer.api.SfsAnalyzerPlugin:com.example.myplugin.MyPlugin -
Build your plugin as a JAR and place it in the
plugins/directory next tosfsanalyzer-app.jar. -
On next launch the plugin appears in the Plugins tab of the settings window (enabled by default).
Each plugin gets its own config namespace persisted in sfsanalyzer.cfg:
CONFIG > PLUGINS > PLUGIN {
name = com.example.myplugin.MyPlugin
disabled = false -- toggled via the Plugins tab
META
{
name,
author,
version,
description
}
SETTINGS -- ctx.getConfig() returns only this
{
...
}
}
Metadata (name, author, version, description) is extracted automatically from the plugin class on first discovery. The plugin's custom configuration lives inside the PLUGIN > SETTINGS sub-node — only this subtree is exposed to ctx.getConfig().
To persist config changes call ctx.markModified(DirtyFlag.CONFIG). A custom settings tab is added when createSettingsNode() returns a non-null node.
- Discovery — all JARs in
plugins/are scanned. New plugins are added tosfsanalyzer.cfgwith metadata. - Filtering — plugins marked
disabled = truein the config are skipped. - Initialisation —
init(PluginContext)is called for each enabled plugin. - Settings —
createSettingsNode()is called lazily when the user opens settings.
Two demo plugins are included in the source repository under demo-plugins/:
- demo-toolbar-button (
ToolbarButtonDemo) — adds a toolbar button that counts parameters in the selected node. DemonstratesaddToolbarButton(),addSelectionListener(),addShortcut(),getConfig(),markModified(), andcreateSettingsNode(). - demo-tab (
SfsViewDemo) — adds a custom tab showing the selected node as formatted SFS source text, controlled by a toggleable toolbar button. DemonstratesaddTab(),TabControl,addToolbarButton()withsetToggleable(), and selection-driven visibility.
Both are built as part of the Maven reactor (mvn clean package from the project root). Their source code is heavily commented — browse it for practical API usage examples with explanations.
| Interface | Purpose |
|---|---|
SfsAnalyzerPlugin |
Entry point. init(PluginContext) is called at startup. |
PluginContext |
Access to the document model, selection events, configuration, dirty tracking, and UI refresh. |
ToolbarButton |
Control a toolbar button. |
ToolbarAction |
Functional interface for the button action ((SfsNode, int modifiers) -> void). |
DirtyFlag |
Flags for markModified(): DOCUMENT, CONFIG. |
RefreshFlag |
Flags for refreshUI(): PARAMETERS, TREE. |
ModifierFlag |
Flags for toolbar click modifiers: CTRL, SHIFT, ALT. |
TabControl |
Control a plugin tab (title, content, visibility). |
The entry point. Every plugin must implement this interface.
| Method | Returns | Description |
|---|---|---|
getId() |
String |
Unique plugin identifier. Default: getClass().getName(). |
getName() |
String |
Human-readable name shown in settings and tab titles. |
getAuthor() |
String |
Author name (default: ""). |
getVersion() |
String |
Version string (default: ""). |
getDescription() |
String |
One-line description (default: ""). |
init(PluginContext) |
void |
Initialise the plugin (toolbar buttons, tabs, listeners). |
createSettingsNode() |
Node or null |
Return a custom settings node, or null to omit the tab. |
The main interaction point between a plugin and the application. Injected via
SfsAnalyzerPlugin.init(PluginContext).
| Method | Returns | Description |
|---|---|---|
addToolbarButton(label, icon, action) |
ToolbarButton |
Add a button to the toolbar before the search field. icon is an Ikonli icon literal (e.g. "fas-star"). |
addTab(title, content) |
TabControl |
Add a tab to the right panel's tab pane below the parameters panel. |
getDocument() |
SfsDocument |
The current document (read/write), or null if no file is open. |
getSelectedNode() |
SfsNode or null |
The currently selected tree node, or null if nothing is selected. |
selectNode(node) |
void |
Selects the given node in the tree and scrolls to it. Does nothing if the node is not found or no document is loaded. |
addShortcut(keyCombo, action) |
void |
Register a global keyboard shortcut. keyCombo is a KeyCombination string, e.g. "Ctrl+Shift+R". Shortcut fires on KEY_RELEASED and takes priority over built-in shortcuts. |
addSelectionListener(consumer) |
void |
Subscribe to selection changes. The consumer receives the new selection or null. |
getConfig() |
SfsNode |
The plugin's private SETTINGS node (persisted in sfsanalyzer.cfg). Empty on first access. |
markModified(flags) |
void |
Signal changes. Use DirtyFlag.DOCUMENT (enables Save) and/or DirtyFlag.CONFIG (persists plugin config and fires listeners). |
isDocumentModified() |
boolean |
Whether the document has unsaved changes. |
refreshUI(flags) |
void |
Request UI repaint. Use RefreshFlag.PARAMETERS (reload parameter panel) and/or RefreshFlag.TREE (rebuild tree). |
addConfigChangeListener(runnable) |
void |
Subscribe to config changes (fires when another source modifies the plugin config). |
removeConfigChangeListener(runnable) |
void |
Unsubscribe a previously registered listener. |
getGameDataPath() |
String |
The configured GameData path from settings, or "" if not set. |
parseFile(file) |
SfsDocument |
Parse any KSP file (.sfs, .craft, .cfg) and return its document model. The document is independent of the currently open file. Throws IOException on read errors. |
Handle for controlling a plugin toolbar button after creation.
| Method | Returns | Description |
|---|---|---|
setEnabled(boolean) |
void |
Enable or disable the button. Disabled buttons are greyed out and do not respond to clicks. |
setVisible(boolean) |
void |
Show or hide the button. Hidden buttons do not take up toolbar space. |
setText(String) |
void |
Change the button's display text. |
setTooltip(String) |
void |
Set tooltip text shown on hover. |
setToggleable(boolean) |
void |
Make the button toggleable (click toggles selected state) or non-toggleable (auto-deselects after every click). Default: non-toggleable. |
isSelected() |
boolean |
Returns whether the toggle button is currently selected (pressed). |
setSelected(boolean) |
void |
Programmatically press or release the button. Only meaningful when toggleable. |
Functional interface for the toolbar button callback:
@FunctionalInterface
public interface ToolbarAction {
void run(SfsNode selectedNode, int modifiers);
}selectedNode is the currently selected tree node at the moment of the click,
or null if nothing is selected. modifiers is a bitmask of
ModifierFlag values active when the button was clicked
(Ctrl, Shift, Alt).
Bitmask flags for toolbar click modifiers.
| Constant | Value | Description |
|---|---|---|
CTRL |
1 |
Control key was held |
SHIFT |
2 |
Shift key was held |
ALT |
4 |
Alt key was held |
Check flags:
if (ModifierFlag.has(mods, ModifierFlag.SHIFT)) {
// Shift was held during click
}Handle for managing a plugin-created tab. Returned by addTab().
| Method | Returns | Description |
|---|---|---|
setTitle(String) |
void |
Change the tab title. |
setContent(Node) |
void |
Replace the tab content. |
setVisible(boolean) |
void |
Show or hide the tab. When all tabs are hidden the tab pane is removed from the layout entirely. |
Bitmask flags for refreshUI().
| Constant | Value | Effect |
|---|---|---|
PARAMETERS |
1 |
Reload the parameter panel for the current node |
TREE |
2 |
Rebuild the entire tree from the document model |
Combine flags:
ctx.refreshUI(RefreshFlag.of(RefreshFlag.PARAMETERS, RefreshFlag.TREE));Bitmask flags for markModified().
| Constant | Value | Effect |
|---|---|---|
DOCUMENT |
1 |
Marks the document as having unsaved changes (enables Save, shows * in title) |
CONFIG |
2 |
Marks the plugin configuration as dirty (persisted on exit) and fires config change listeners |
Combine flags:
ctx.markModified(DirtyFlag.of(DirtyFlag.DOCUMENT, DirtyFlag.CONFIG));All model classes live in io.github.crvx.sfsanalyzer.model.
A named node with an ordered list of entries (parameters, comments, children).
| Method | Description |
|---|---|
getName() |
Node name (e.g. "VESSEL") |
getEntries() |
Mutable List<SfsEntry> (params, comments, children) |
addEntry(SfsEntry) |
Append an entry |
getChildren() |
List of direct child SfsNodes (filtered from entries) |
getFlatValues() |
LinkedHashMap<String, String> of key-value parameters |
getComment() / setComment(String) |
Optional node comment |
deepClone() |
Deep copy of the node and subtree |
queryNode(path) |
Navigate a "/"-separated path to the first matching descendant, or null. Empty segments are ignored. |
queryAllNodes(path) |
Navigate a "/"-separated path and return all descendants matching the final segment (never null). |
| Record | Fields |
|---|---|
SfsEntry.Parameter |
String key, String value |
SfsEntry.Comment |
String text (includes // prefix from the parser) |
SfsEntry.Child |
SfsNode node |
| Method | Description |
|---|---|
getSourceFile() |
The source file (or null) |
getRoot() / setRoot(SfsNode) |
Root node of the tree |
isVirtualRoot() / setVirtualRoot(boolean) |
Whether root is a synthetic wrapper |
isModified() / markModified() / clearModified() |
Dirty-tracking flag |
unwrapIfNeeded() |
Normalise virtual-root wrapper (called automatically after parsing) |
Toolbar and UI icons use Ikonli with the FontAwesome 6 set. Each icon is referenced by its Ikonli literal string (e.g. "fas-star") which maps to a constant in org.kordamp.ikonli.fontawesome6.FontAwesomeSolid.
Browse all available icons at the FontAwesome 6 Cheat Sheet.
MIT License. Copyright © Sergey Kuznetsov (crvx).



























