flutter3d_plugin_api 1.0.0-rc.1
flutter3d_plugin_api: ^1.0.0-rc.1 copied to clipboard
The interfaces a flutter3d plugin is written against: the manifest, the host and its registries, the loop's phases and the event bus. Plain Dart over flutter3d_foundation.
1.0.0-rc.1 #
-
New:
tools.dart, what a plugin hands an agent.McpTools,McpToolRegistry,ProjectTool,ProjectToolRun,ToolCall,SessionToolCall,ToolSpec,ToolHints,ToolResultand its content types live here, over this package alone, andflutter3d_mcpre-exports them under the imports it had. A plugin that brings tools no longer puts the agent servers into the game that ships it. -
PluginApiVersion.currentsays when it moves. Its minor moves in the release that adds anything a plugin can call, in this package,flutter3d_fields,flutter3d_plugin_runtimeor a registry another package hands a plugin, and a structure rule holds it to that from the first 1.x release on. A plugin writes the number it was written against as a literal, nevercurrent. -
Breaking: every engine plugin is named after the package that ships it, and
isEngineis the namespace alone.PluginManifest.engineNamespacesholds the packages this repository publishes and published at 0.8.5, and no longer the never-publishedflutter3d_addon_*names orflutter3d_demo_content. The plugins of the 0.8 add-ons areflutter3d_post.bloom(and its 22 siblings),flutter3d_camera,flutter3d_game_kit.reactions,flutter3d_game_kit.soundtrackandflutter3d_game_ui.spoken_events, andAudioPluginisflutter3d_audio_core, where it wasflutter3d_audio.PluginChange.fromJson,SimulationVersion.fromJsonand a manifest'sdependsOnread the old ids as the new ones, so a run or a.f3dpluginwritten before still opens; a plugin from outside may not take an add-on's namespace (idProblem). -
reservedAreasis frozen at what 1.0.0 ships. A test holds it to the list; an area a 1.x minor brings goes underf3d., or under a name with an upper-case letter, which no plugin id can have. -
A plugin's simulation profile is named in its namespace. Bare profile names are the engine's; a subsystem a plugin registers under a bare profile the engine does not define, or another namespace's, is refused (
SimulationProfile). -
PluginManifest.engineNamespacesnamesflutter3d_pointer_lock, the namepointer_lockis published under from 0.4.3 on. The old name stays in the set, so a recorded run that names it still reads as the engine's. -
Breaking: a codec, a snapshot part and a data section take one context each.
ComponentCodec.decode,EventCodec.decodeandSnapshotPart.restoretake aComponentDecodeContext,EventDecodeContextandSnapshotRestoreContext(new) holding the data and the version it was written at;InPlaceCodec.restoreIntotakes aComponentRestoreContext(new) with the component too, andInPlaceCodec.restoreAny(new) types one for a registry that holds codecs asInPlaceCodec<Object>.DataSection.read,loadandinstalltake aDataSectionRead,DataSectionLoadandDataSectionInstall(new), each with the step's input and theDataSectionContext. The.offactories' callbacks take the same contexts. A parameter added to any of them later would have broken every codec and section written against 1.0; a member added to a context breaks none. -
Breaking: a queued spawn's builder takes one
SimSpawned.SimCommands.spawnhands its builder aSimSpawned(new), the entity and its world, rather than the two as parameters. -
LightType,RigidPoseandSeveritycome with the contract. The foundation types a plugin's signatures name now include anIssue's severity, a light's type and a pose, so a plugin gets them from this one import like the rest. -
The engine is a list of packages, not a prefix.
isEnginereadsPluginManifest.engineNamespaces(new), the packages this repository publishes and the ones it published before 1.0.0 under other names, so a package somebody else callsflutter3d_frostregisters in its namespace like any plugin from outside. -
A plugin may not be named after an area of the engine.
idProblemrefuses a plugin from outside whose namespace is one ofPluginManifest.reservedAreas(new): the areas of the engine's MCP tools, VM extensions and events (light,render,physics, …) and the ones a minor is expected to bring, so a tool or an extension the engine adds to an area never shadows a plugin's. -
A simulation keeps what a newer build wrote.
SimulationVersion.extra(new) holds the keysfromJsondoes not know andtoJsonwrites them back;==andrefusalOncompare them, so a run or a hello from a simulation with a part this build has no field for is refused with the key named rather than read as this build's. -
A plugin may touch only tooling.
PluginTouches.tooling(new) is a plugin for the editor and its tools: it leavesSimulationVersionalone as a view plugin does, and the hosts refuse it a step phase, a step system or subscription, a subsystem, a render step or node and shader stages, so a game need not install it. -
One namespace for every name, the editor's and the agent's too.
PluginManifest.qualify(new) answers a name in the plugin's namespace,<namespace>.<name>for a bare one, and refuses one in another namespace with thePluginExceptionnameProblemwords; the editor's pieces and panels and a plugin's MCP tools are filed by it. -
A plugin describes its fields to an editor without depending on one.
package:flutter3d_plugin_api/editor.dart(new) holdsFieldHint(a field's control: a number with ends, a step and a unit, a toggle, a choice, a colour, a vector, text, an asset path, an entity),FieldKind(an open set),ComponentHints,EntityKindHintsandMaterialHints, each read and written as JSON, andEditorHintRegistry, the slot an editor fills and a game's engine does not have:host.maybeRegistry<EditorHintRegistry>(). -
A run recorded under an outside profile reads back.
SimulationVersion.fromJson(profiles:)reads a profile by its name among the ones given, this release's by default; a name none of them has is read under that name promising nothing (not bit-exact, no double precision, no adaptive steps) instead of refusing the file, and a version matches another by its profile's name, since the name is all a file says of it. -
A simulation names the subsystems that run in it.
SimulationVersion.subsystemsholds every running solver's id at its own number (withSubsystems), written only when there is one, so the default simulation's run files and hellos read as before; a run recorded with a subsystem running is refused where it is not, and the other way round, with the subsystem named. -
Scene bindings, as data.
SceneBindingSpecsays how a component of the simulation is shown: the kind of node (an open name), the material, which fields drive which material parameters,BindingInterpolationandBindingLifetime; it reads and writes a.f3dplugin'sbindings:entry and keeps the keys it does not know.SceneBindingRegistryis where a plugin registers one. -
Every name a plugin registers is in its namespace.
PluginManifest.namespaceis the id up to its first dot — the package that ships the plugin — andnameProblemsays why a name outside<namespace>.may not be registered;f3dis the engine's own, and the engine's packages (isEngine) register the engine's names. The format registry holds a plugin to the same namespace (it held it to the whole id before, which every id it took is still in). Names compare case-sensitively everywhere. -
A registry brings its companions.
PluginRegistry.companionslists the registries that come with one, andPluginHost.registryfinds them as if each had been handed to the host: an engine that hands itrenderer.renderStepsgives a plugin the renderer'sLightingModelstoo. -
PluginGroup: plugins taken whole and adjusted.setputs a configured plugin in place of the member with its id,addappends one,without<T>()andwithoutIdtake members out, each answering a new group. A package's own group is a class extending it (StandardPlugins()), and the project's generatedinstalledPluginsis one. A member a later release drops is not an error tosetorwithout.PluginManager.installGroupinstalls one in its order. -
SimulationProfile, what a simulation promises about its numbers.deterministic(bit-exact on every platform) andaccurate(double precision, adaptive steps), with wire names of their own and an open set: code asks a capability (isBitExact,usesDoublePrecision,allowsAdaptiveSteps), never which profile it is.parserefuses a name it does not know. A plugin is told its engine's asPluginHost.profile(deterministicunless the host says otherwise), which aPluginManagertakes asprofile:. -
LoopPhase.elementsis gone; useLoopPhase.fields. A loop still finds the phase by the nameelements(LoopPhase.formerNames). -
DataSection,DataSectionContextandDataSectionRegistry. A subsystem registers a reader for a named section of a.f3dplugindocument, and the runtime that reads the document hands it what was written there, so the runtime depends on no subsystem it installs into. -
The contract only: the foundation and the simulation host moved out.
Flutter3dExceptionand its four families,DocumentFormatException,WorldPosition,LinearColor,Issue,IssueSink,FormatSpec,FormatDocument,FormatMigration,FormatRefusalandRegistrationare declared influtter3d_foundation, this package's one dependency, and re-exported here under the same names, so a plugin changes nothing.SimulationHandle,SimulationAnswer,SimulationQueryRegistryandSimulationCapabilityExceptionareflutter3d_sim's, beside the loop that fills them; the registry slots are declared by the package that fills each (DecoderRegistryinflutter3d_core,EntityKindRegistryinflutter3d_sim,EditorRegistryinflutter3d_editor_core,McpToolRegistryinflutter3d_mcp). What stays is what a plugin is, what it is handed and what a step publishes (PublishedState,PublishedEvent,SimulationVersion,WorldPositionCodec). -
LoopPhase.fields, the phase heat, water, wind and fire spread in, named for what runs in it rather than for the package that filled it.LoopPhase.elementsis deprecated and isfields; a loop finds the phase by its old name too (LoopPhase.formerNames,currentName,current), so a data plugin's file that says"phase": "elements"still installs. -
PublishedState.toWireandPublishedState.fromWire: a published state as plain values and back, for a handle that crosses an isolate or a socket. A value that is not one is refused with aPluginFormatException. -
Breaking: every thrown type is named
*Exception(decision H).FormatRefusedisDocumentFormatException.ConstraintCycleErrorisConstraintCycleException, aPluginExceptionrather than anError, because a cycle comes from plugins' and a project's own constraints. AFormatRegistryclash, a plugin's format id or alias outside its namespace and a plugin's format with no fixture throwFormatRegistrationException, aPluginException, where they threw aStateError. -
Breaking:
FormatSpec.binaryisenveloped, true by default: whether a document starts with the JSON envelope..f3dmatis text without one, whichbinarymisnamed. -
Breaking:
PluginPermission.simulationis gone. Nothing enforced it; a manifest that names it still reads, as an open permission. -
A plugin's format aliases are held to its namespace as its ids are.
-
PluginTouches.namedkeeps a kind it does not know, under its own name and held to the simulation's rules, so a manifest read and written again says what it said.PluginTouches.isKnowntells the two apart. -
VM extensions can be renamed without breaking callers, and say what they answer.
registerFlutter3dExtensionandVmExtensions.registertakealiases:, old names that keep answering until the next major and are listed underaliasesinext.flutter3d.version, andanswers:, the keys of the JSON object the handler answers with, whichapi/<package>.vmsnapshots. -
The snapshot registry is the one path for state, and says so in its shape.
SnapshotRegistrygainscapture,restoreanddigest, so a tool that rewinds, replays or checks a run is handed the registry rather than functions of its own;restorethrows aStateErrorfor a state that holds none of its parts.SnapshotPart.restoresFirstputs a frame the others are relative to — a physics world's floating origin — back before them. -
The view reads the simulation through published state only (decision A of
tasks/1.0-arch-review.md).LoopContext.publishedis what a frame phase reads;LoopContext.worldis the step phases', and the engine's loop throws for it in a frame phase.PublishedState.eventsarePublishedEvents, encoded by each event's declared codec and read back withdecode, so a published state crosses an isolate as plain values.SimulationHandle.askasks a question by name, answered where the world is from aSimulationQueryRegistry;query, which takes a closure, is documented as the same isolate group's shortcut. A step-channel event is declared with its codec: the engine's bus asserts it.OriginShifted.codecandeventNameare new, and the engine declares it. -
SimulationVersionnames every genre a run was played by:otherGenresbesidegenre, written asotherGenresin a run file and compared like the rest;genreslists them all. -
IssueandIssueSinkare re-exported here (declared influtter3d_foundation), the one type a library reports what it could not do in, so the audio packages, which name no application, report in the same typeflutter3d_appdoes. -
Breaking: a boolean reads as a question, and no
boolis positional.LoopContext.resimulatedisisResimulated;SimWorld.aliveisisAlive.dart fixcarries the renames. -
Breaking: American spelling in identifiers, as Flutter and Dart use.
isCancelledisisCanceled. Only the Dart names changed: a file keeps the keys it was written with, anddart fixcarries the renames. -
A plugin may add anchors of its own.
RenderAnchor.after(anchor, '<pluginId>.<name>')stands right after another anchor; a renderer'sRendererSteps.addAnchormakes it placeable.follows,isEngineandengineAnchorsay where one stands. An engine anchor'snameis a wire name a.f3dpluginfile stores, and never changes within a major. -
The simulation's model is in the plugin API: entities, components and their codecs.
Entitymoved here;ComponentCodec<T>(id, version, encode, decode) andInPlaceCodec<T>say how a component is written for a snapshot, the network, a prefab or the editor;ComponentRegistryfiles them, with apublishedflag that is the boundary to the view;SimWorldis the world a system reaches asLoopContext.world: spawn and despawn, get and set, a query builder (having,without,changed), resources and commands deferred to the end of the phase. -
One path for state:
SnapshotPartandSnapshotRegistry. A plugin that keeps state outside the world registers a part (id, version, capture, restore, digest), and every rewind, rollback, replay check and scrub covers it. -
The view reads
PublishedStatethrough aSimulationHandle. Immutable, built after thepublishphase: published components as their codecs wrote them, positions asWorldPosition, the step's events. The handle (published,submit(input, seat),query,rewindTo) has one shape whether the world is in this isolate or another.OriginShiftedis the bus event a floating origin moves with. -
Plugins have versions, and dependencies have ranges.
PluginManifest.version(PluginVersion),simulationVersion, anddependsOnentries that may carry a range:'heat ^1.2.0'(PluginDependency,VersionRange). The manager refuses a dependency outside its range with both numbers named, and reports every simulation plugin's number (PluginManager.simulationVersions). -
SimulationVersionlives here, withplugins. Moved fromflutter3d_sim, which no longer exports it, so a network hello and a manifest reach it; a run, a hello and a relay compare the engine's number, the genre's and every simulation plugin's. -
Events can have codecs.
EventRegistry.declare<T>(…, codec:)with anEventCodec<T>; a declared codec is what an event's digest is taken of, andcodecForreads it back.onRetractedmoved ontoEventRegistry. -
Breaking: what the plugin API reads from text throws
PluginFormatException, aFlutter3dFormatException, where it threw a bareFormatException: a manifest, a plugin change, a version, a range. -
The engine's VM service extensions have one door and a version.
registerFlutter3dExtensionregisters anext.flutter3d.*extension and, the first time,ext.flutter3d.version, which answersvmSchemaVersionand the name of every extension registered so.VmExtensionsis the registry a plugin adds its own through: a plugin's extension isext.flutter3d.<pluginId>.<verb>, and switching the plugin off switches it off (the VM service cannot forget a name, so it answers an error until the plugin is back). -
PluginPermission.tools. A plugin that offers tools to an agent says so in its manifest;McpTools.addToolrefuses one that did not. -
One envelope for every JSON format, and a registry that knows them.
FormatSpecdeclares a format once, beside its reader: its id (f3d.<kind>, or<pluginId>.<kind>for a plugin's), suffixes, the version it writes, the migrations that lift older documents, and the fixture minted at each version.openrefuses another format's document, a version from the future and arequiresit does not understand, each with a sentence through the format's own exception, and lifts the rest;envelopewrites{"format", "version", "requires", "generator"}. The shapes from before the envelope ({"version": N}, a format's own key such as{"f3dfx": 1}, an old id listed inaliases) read as version 1.FormatDocumentis the base every document type extends to keep the top-level keys it did not read and write them back.FormatRegistryfinds a format by id, suffix or first bytes, and is aPluginRegistry: a plugin's formats are held to its namespace and must name a fixture. -
PluginPermission.tools: a plugin that offers tools to an agent asks for it, andMcpTools.addToolrefuses one that did not. -
One root for every exception:
Flutter3dException. It has amessageand an optionalcause, and four families under it, each anabstract base classa package can add its own leaf to:Flutter3dFormatException(bytes or text that are not what they claim, named with the prefix so it never shadowsdart:core'sFormatException),CapabilityException(a device or platform without a feature),PluginExceptionandResourceException(a file, tool, device or service the engine needed and did not get).PluginExceptionis the family and still a class you can throw, as it was; it moved here from the manager's file and is nowbaserather thanfinal, so the run-time plugins' refusals extend it. -
WorldPosition: a place in the world in double precision. Three doubles in metres, Y up, right-handed, immutable, withtranslated,relativeTo(the offset from an origin, still in doubles),distanceTo,lerpand value equality. Float32vector_mathstays in local spaces, the view and GPU buffers;flutter3d_foundationhas the conversions, each relative to an origin you name. Nothing takes it yet: the simulation and the scene graph move onto it next. -
LinearColor: the engine's one colour type. Red, green, blue and alpha in linear light, unclamped above 1, withfromSrgbandtoSrgbthrough the exact sRGB curve,withAlpha,scaled,lerpand theblack,whiteandtransparentconstants. A FlutterColorconverts influtter3d. Nothing takes it yet either. -
1.0.0 is a promise: strict semver from there. This release candidate already keeps it. A patch fixes bugs and breaks nothing, a minor adds, and a break waits for a major. The whole public API is stable, with no experimental exceptions, and is held to the snapshot in
api/. A deprecated name stays until the next major and for at least six months, and says what replaces it. CONTRIBUTING.md has the rules, and SUPPORT.md says which releases get fixes and on which platforms. -
The first plugin API, and its own version line. A plugin depends on this package alone:
Flutter3dPlugin, itsPluginManifest, and thePluginHostit is installed through. The API version a plugin names is checked beforeinstallruns. A different major, or a newer minor than the engine provides, is refused with a sentence saying which to update. -
One host, keyed by ids.
PluginManagerchecks ids and versions, installs plugins in dependency order and keeps the given order wherever dependencies leave a choice. A cycle is aConstraintCycleExceptionthat names the plugins in it, and a missing dependency names both sides. It switches plugins on and off, and reorders them, only at a step boundary, and journals each change with its step so a replay makes it at the same one. -
The loop's phases and the bus's types.
LoopPhasenames the engine's step and frame phases, andLoopRegistryadds a plugin's own phases and systems withafter/beforeconstraints.BusEvent,DeliveredandEventRegistryare the typed bus with its step and frame channels. -
Slots for what later releases open.
RenderStepRegistry,DecoderRegistry,EntityKindRegistry,EditorRegistryandMcpToolRegistryare declared now and filled by the engine's packages as each is opened, so a plugin's API version already knows their types. -
Render anchors, and the first slot filled.
RenderAnchornames the places in the frame a plugin's pass may go: before and after every pass the engine draws, frombeforeShadowstobeforePresent, fifty in all, in frame order inRenderAnchor.values. It is an open value class, and the names hold for the whole major.RenderStepRegistry.anchorslists them. The renderer influtter3d_corefills the slot withRendererSteps, which adds render steps and places nodes at anchors. -
Every other slot filled, and this package unchanged for it. Each slot is filled the way the render one is, by a type in the package that knows what it registers:
ModelDecoders(flutter3d_core) andDecoders(flutter3d) forDecoderRegistry,EntityKinds(flutter3d_sim) forEntityKindRegistry,EditorPieces(flutter3d_editor_core) forEditorRegistry, andMcpTools(flutter3d_mcp_kit) forMcpToolRegistry, whose tools are published as<plugin id>.<tool>. A registry with no slot here is still found by type: behaviour-tree kinds areBehaviorKindsRegistryinflutter3d_sim. The slots' own declarations only gained documentation naming who fills them. -
Built to grow without breaking. Registries, the host and the loop context are
baseclasses, so a new member arrives with a default. The kinds a later minor may extend are open value classes rather than enums. A manifest read from a document keeps the keys it does not know.