How it works
The shape of the code
addons/daelokbase/core/ holds the parts that know nothing of the editor's controls: each a class_name Dlb… of static functions, taking values and giving values back, checked headless with no editor at all. addons/daelokbase/ui/ holds the Data tab and everything drawn: a Control each, calling into the core. plugin.gd makes the screen, registers the inspector plugin, the thumbnail generator, the hover cards and the dock's menu, and takes them out again on exit. bridge_ops.gd is the module the Editor Bridge picks up.
| module | what it is for |
|---|---|
DlbIndex |
finding the collections: the project's global classes that extend Resource, their metadata, their folders, their entry files |
DlbSchema |
fields to @export lines and back; the class script with its region; the hash of the region |
DlbMetadata |
the JSON under .daelokbase/schemas/ |
DlbEntries |
making, naming, saving (UID kept), moving and binning entry files; a plain value assigned as the property wants it |
DlbNaming |
identifiers, slugs, PascalCase and snake_case, Godot's reserved names |
DlbQuery |
the filter language |
DlbLayout |
the form's tree of containers and fields, every change a copy |
DlbMigration |
what a schema change does to every entry, the words for it, the doing of it, snapshots and their restore |
DlbValidate |
the problems, sorted and counted |
DlbBacklinks |
who points at whom |
DlbCanvasLayout |
where the boxes and chips go, and what is under the cursor |
DlbGameIcons |
the icon list, the search, the recolouring, the credit tag |
DlbPreview |
what a preview scene's root must have; a render of it to an image |
DlbLoaderGen |
the loader script |
DlbImporter |
a Classic project as a plan, then as scripts, metadata and entries |
DlbPlain |
values as JSON, a CSV or the bridge carry them, both ways |
DlbTableFile |
a collection as a CSV or JSON file, out and in, with the plan of an import |
Finding the collections
DlbIndex.collections() reads the project's global class list, keeps the classes that extend Resource and are not an addon's, and pairs each with its metadata. A class with no metadata is an adopted one: its fields are read back from its script's exports, its entries are its files in the folders scanned (DlbProject: the whole project unless chosen otherwise), and its folder is the deepest one holding them all; outside the folders scanned it is left out, unless its script is in one. A collection DaelokBase manages has a folder in its metadata, and its entries are the files of its class in that folder and the folders under it. A file's class is read from its header, and kept while the file is unchanged. The project's files are walked once a frame however many views ask for them, which keeps a reload of the Data tab to a quarter of a second in a project the size of Oddside, and a file DaelokBase writes, moves or bins is seen at once. A new entry goes into the collection's folder when entries sit in it, else beside the most of them; a renamed one stays in its folder, a copy goes beside its original. Hidden collections are a choice of the Data tab alone: the tree folds them under Hidden, and the canvas and the Problems panel leave them out. Nesting in the tree is by folder first, then by what the class extends. The tree is rebuilt whenever the editor reports that the file system or the class list changed, which is after every save.
The form
The laid-out form walks the layout tree and builds a Control per node: a box for a container, a foldable section for a labelled one, and for a field the editor of its kind. A scalar gets the editor Godot itself would use, made with EditorInspector.instantiate_property_editor and bound to the entry. A resource gets an EditorResourcePicker. A list or a group gets a small EditorInspector of its own, editing a proxy object that declares just that one property and forwards its reads and writes to the entry, so Godot's own array and sub-resource editors do the work. A soft reference gets an OptionButton of the target's entries. The plain inspector is Godot's EditorInspector on the entry, trimmed by an EditorInspectorPlugin that hides the Resource section and swaps in the same soft-reference picker.
What a change does
- The editor for the field emits its change. The form records an undo action holding the old and the new value on the entry, so Ctrl+Z works as anywhere in the editor; the changes of a field made within a moment of each other merge into one action, as the letters of a word typed do in Godot's inspector.
- The screen saves the entry. The UID in the file's header is compared before and after, and put back if the save dropped it.
- The FileSystem dock is told about the file, which makes the editor report a change; the screen then refreshes its tree, the badges and the Problems panel, and the other views. The form is rebuilt only when the entry, its script's fields or the collection's metadata changed; otherwise it is left as it is, so a text box being typed in keeps its focus.
- When the changed field is the key, the file is renamed after it and the resource told its new path, so the UID travels with it. While the key is being typed, the rename waits until the text box loses its focus, so the file is renamed once rather than at every letter.
An undo or redo comes in as a change of the undo history's version. The entry's values are compared with those last saved, and the file follows when they differ, its name too when the key is among them.
The schema designer
The fields the designer shows are turned into a script by DlbSchema: the region with an export line per field, everything outside it kept. The designer shows that script before anything happens. Apply asks DlbMigration for the plan: the changes between the old fields and the new (added, renamed, retyped, a default changed, retired), and for every loaded entry what each change does to its value, with a note where a conversion loses something. The plan is shown in words in a dialog.
On confirm, DlbMigration.perform takes a snapshot, writes the script, reloads the class in place (Godot keeps the loaded instances, and classes extending the changed one are reloaded too), sets the values on every loaded entry (the carried and converted ones, the defaults of added fields, the old default where it is pinned), saves every entry file and the metadata, and tells the dock. A group's columns are migrated the same way, the rows of every entry taken as if they were entries of the row class; a new row class is written and scanned before the collection's class gains its typed array, which is why Apply may wait a moment.
Revert copies the snapshot's files back, reloads the restored scripts, and re-reads every restored entry in place: each managed member is set back to its default, then the file is loaded with the cache replaced, so the very object the editor holds shows the file's values. Files the snapshot notes as made by an import go to the bin. The snapshot is then discarded.
The validator and the Problems panel
DlbValidate loads every entry of every collection and checks the keys, the file names, the list sizes, the enum values and the soft references, in a group's rows too, and sorts what it finds errors first. The Problems panel runs it whenever the screen refreshes, badges the tree, and on a click brings the Data tab to the front (a hidden inspector builds no rows), selects the entry, and finds the property editor for the field on whichever form is showing, to select it and scroll it into view.
Backlinks
The links between collections come from the metadata alone: every reference field with a known target. Finding who points at one entry loads only the collections that have a reference field, and compares keys for soft references and file paths for hard ones. The canvas draws the links as arcs between the headers; the Stage lists them under Used by; the bridge answers them.
The canvas
DlbCanvasLayout places the boxes: chips four to a row, nested collections as boxes inside the body, roots in a row from the top corner unless pinned, and answers what is under a point. The view draws the whole map in one _draw with the editor's own fonts and colours, so no chip is a Control and a thousand cost nothing. The colours are the hash DaelokBase Classic used, reproduced, so a project brought across keeps them.
The Stage
Pictures, sounds and BBCode are shown with plain controls, the BBCode by a RichTextLabel after fonts named rather than held by the project and images on the web are taken out. The preview scene is instantiated once into a SubViewport with a world of its own and handed the entry through dlb_preview(entry) every time the entry shown changes or is edited; it stays instantiated until the scene changes.
The FileSystem dock
The thumbnail generator draws its card with the RenderingServer alone, into a viewport of its own, and reads the image back, because the editor asks for previews inside its own step: on the main thread with the Compatibility renderer, where nothing that needs a frame can be drawn, and from a preview thread with the other renderers, where the scene tree may not be touched. The hover card is an EditorResourceTooltipPlugin registered on the dock; Open in DaelokBase is an EditorContextMenuPlugin in the dock's slot, which fronts the Data tab and selects the entry.
The game icons
The icon list is the repository's tree, fetched once from GitHub and kept in the editor's cache folder along with every SVG fetched since; a bundled list of 152 stands in when there is no connection. Placing an icon recolours the SVG's text, adds a credit tag, writes it beside the entries, waits for the editor's import of it, loads it, and sets it on the entry as one undo action.
Import and export
The Classic importer reads the project's JSON into a plan (classes, folders, fields, groups, layouts, entries), writes the scripts, waits for the scan, then makes every entry through the same functions the form uses, so files get their UIDs from the editor. DlbTableFile writes a collection as CSV or JSON from DlbPlain's plain values, reads such a file back, matches rows to entries by key, plans the changes, and applies them after a snapshot that notes the new files. DlbPlain is also what the bridge speaks: an entry's values out as plain text, numbers, lists and maps, and plain values in as what the fields want, with every problem named.
The bridge module
The Editor Bridge looks for addons/<name>/bridge_ops.gd in every addon, instantiates it, and merges its commands() and help() into its own table. Each command is a function of (args, ok, fail) that answers through one of the two callables. The module needs no bridge to work: the Data tab's tests call its commands directly. After a command that changed something, the Data tab, when it is open, is refreshed and shown the entry.
The tests
python tests/run.py copies the project to a scratch folder with a scratch APPDATA, runs an import pass, then the core checks as a headless SceneTree script (tests/core_test.gd), then the editor checks inside a headless editor (tests/editor_test.gd, started by the addons/daelokbase_tests hook with -- --dlb-test). It fails on any check that fails and on any error the engine prints. --all runs it on 4.5.1, 4.6.2 and 4.7.2. The editor checks import the example project into real classes and entries and then drive every view, every command of the bridge module, every migration and its revert.
Three things worth knowing
- A class that is not
@toolis a placeholder inside the editor: its instances answer every property, but a member the class gained after an entry was loaded answers null until it is set, and its property list carries no hints. The plugin reads fields from the script and sets added fields' defaults after a migration for that reason. - An
EditorInspectorbuilds its rows only while it is visible in the tree, in its process step. Anything that wants a field's editor brings the Data tab to the front first. - Under the Compatibility renderer the editor asks for a resource preview on the main thread, inside its own step, and no frame passes until the answer is given. That is why thumbnails are drawn with the RenderingServer and forced, and why the preview scene draws in the Stage rather than in the dock.