一键导入
starsector-architecture
Core system architecture including EventBus, Undo/Redo, Layer System, Threading Model, and Startup Sequence.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Core system architecture including EventBus, Undo/Redo, Layer System, Threading Model, and Startup Sequence.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Core guidelines, coordinate transformation mathematics, high-performance drawing techniques, and state recovery patterns for OpenGL rendering in Starsector Ship Editor.
Guidelines for Jackson configuration, JSON pre-processing, CSV serialization rules, and entity ID extraction in Starsector Ship Editor.
SQLite database design, indexing engine, transaction strategies, and query service in Starsector Ship Editor.
Information about the required technology stack, environment JVM flags, and build plugins.
Guidelines for JUnit, jqwik property-based testing, SpotBugs static analysis, and verification rules.
| name | starsector-architecture |
| description | Core system architecture including EventBus, Undo/Redo, Layer System, Threading Model, and Startup Sequence. |
This skill is organized as follows:
SKILL.md: Main instructions (this file).resources/: Configurations and templates.
examples/: Code references.
scripts/: Tooling.
EventBus.java is the backbone of inter-component communication. It is a custom, minimal implementation — not Google Guava EventBus or GreenRobot.
Floating Subscribers: Stored in a WeakHashMap-backed Set. The listener itself is the weak key. If no strong reference exists elsewhere, the listener is GC'd and silently unsubscribed.
subscribe(listener) will be immediately eligible for GC unless the caller retains a reference. The Javadoc explicitly warns about this.Lifecycle Subscribers: Stored in a WeakHashMap<Object, CopyOnWriteArrayList<BusEventListener>>. The parent component is the weak key. All listeners bound to a parent are kept alive as long as the parent is alive.
subscribe(lifecycleParent, listener) — Binds the listener's lifetime to the parent.unsubscribeByParent(parent) — Removes all listeners for a parent in O(1).Reentrant-Safe Dispatch: Events can trigger other events during handling (e.g., selecting a layer fires LayerWasSelected, which might trigger ViewerRepaintQueued). The EventBus handles this via a ThreadLocal<DispatchContext>:
private static class DispatchContext {
final List<List<BusEventListener>> depthLists = new ArrayList<>();
int currentDepth = 0;
}
Each nesting level of publish() gets its own snapshot list, preventing ConcurrentModificationException. The depth counter tracks reentrant calls.
Broadcast, Not Selective: Every event is broadcast to every listener. There is no type-based filtering at the bus level — each listener must instanceof-check the event type. This was a deliberate KISS design decision; an earlier implementation with generics and selective dispatch caused "more issues down the road" (per the source comment).
Error Isolation: If a listener throws, the exception is caught, logged with the listener's class name (cleaned of lambda hex suffixes via regex), and dispatch continues to the remaining listeners.
UndoOverseer.java implements a strict LIFO undo/redo stack.
private static final UndoOverseer seer = new UndoOverseer();ArrayDeque<Edit> instances — undoStack and redoStack.MAX_UNDO_CAPACITY = 200. When exceeded, the oldest edit is silently dropped from the bottom of the undo stack.UndoOverseer.post(edit) pushes to undo stack, clears redo stack, marks the active layer as dirty.edit.undo(), pushes to redo.edit.redo(), pushes to undo.When a layer is removed, cleanupRemovedLayer(painter) iterates both stacks and removes all LayerEdit instances referencing that painter. This prevents undo/redo from operating on deleted layers. The edit's cleanupReferences() is called to null out dangling references.
adjustPointEditsOffset(point, offset) iterates all edits (both stacks) and adjusts the stored positions of PointDragEdits. This is needed when a layer's anchor changes — existing undo history must be updated to reflect the new coordinate system.
Every edit categorizes itself as EditCategory.NONE, EditCategory.HULL, or EditCategory.VARIANT. Non-NONE edits mark the active layer as unsaved (hull or variant type), which enables the "unsaved changes" indicator in the UI.
The class comment explicitly states: "exposing edit stack to the user for selective undo is strictly forbidden." The system relies on strict LIFO ordering; cherry-picking edits would break state consistency.
ViewerLayer → LayerPainter RelationshipEach ViewerLayer (abstract) may have a LayerPainter attached. Concrete implementations:
ShipLayer → ShipPainterWeaponLayer → WeaponPainterWhen loading a new layer while others exist, PrimaryViewer.loadLayer() automatically positions the new layer's anchor adjacent to the previous layer:
var layerAnchor = layerPainter.getAnchor();
var prevLayerWidth = layerPainter.getSpriteSize();
Point2D widthPoint = new Point2D.Double(
layerAnchor.getX() + prevLayerWidth.width, layerAnchor.getY()
);
newPainter.updateAnchorOffset(widthPoint);
UndoOverseer.finishAllEdits(); // Mark all existing edits as non-combinable
This places sprites side-by-side for visual comparison. finishAllEdits() is called to prevent the anchor update from being combined with previous drag operations in the undo stack.
All Swing UI modifications must happen on the Event Dispatch Thread. Main.main() wraps the entire initialization in SwingUtilities.invokeLater(). The IndexScannerTask uses SwingUtilities.invokeAndWait() to show confirmation dialogs from the background scanner thread.
Because AWTGLCanvas.paintGL() is called during Swing's repaint cycle, the GL thread is the EDT. This means:
paintGL() and the glRunnables queue.queueGLTask().IndexScannerTask.scanAndIndexAll() runs on a background thread (launched by the data loading system).CompletableFuture queries in DatabaseQueryService run on ForkJoinPool.commonPool().ConcurrentHashMap or thread-confined collection, rather than mutating the active global collections in GameDataRepository directly.Runnable that assigns the fully populated local map to GameDataRepository atomically on the EDT.GameDataRepository and FileLoading (such as allShipEntries, allWeaponEntries, allVariants, allProjectiles, and loadingInProgress) must be marked volatile to guarantee immediate visibility of the references/values across thread boundaries.lifecycleSubscribers) must protect all operations (such as computeIfAbsent() in subscribe and remove() in unsubscribeByParent) with explicit synchronization (synchronized (bus.lifecycleSubscribers)) to prevent map corruption from concurrent access.SwingUtilities.invokeLater() before touching UI components.StaticController is a static singleton that holds references to the active PrimaryViewer, LayerManager, and active layer. It acts as a global service locator.
This is a pragmatic trade-off: it avoids threading constructor references through deeply nested component hierarchies, at the cost of testability. The entire application assumes a single-viewer, single-window architecture.
Settings are stored in ship_editor_settings.json (co-located with the JAR). The SettingsManager manages serialization/deserialization of the Settings POJO via Jackson.
Key settings include:
The SQLite database is placed in the same directory as the settings file, discovered via SettingsManager.getSettingsPath().toPath().getParent().
Main.main() executes:
checkAndRelaunch() — Self-relaunch with memory cap if needed.sun.java2d.opengl=false, sun.java2d.d3d=false.Locale.US to ensure consistent number formatting.PrimaryWindow.
f. Update state from settings.
g. If loadDataAtStart: show window, then FileLoading.loadGameData() (triggers IndexScannerTask).
h. Else: show window immediately.The source comment explicitly states: "These method calls are initialization block; the order of calls is important." For example, settings must be initialized before configuring the LAF (which reads the theme preference), and the window must exist before loading game data (which may show progress dialogs parented to the window).