The Modules Format - #8408
Closed
keithharvey wants to merge 3 commits into
Closed
Conversation
This was referenced Jul 22, 2026
Closed
Closed
Closed
Collaborator
Author
|
Synced with the format decision from the bar_editor track: framework-shared types now live in root |
keithharvey
force-pushed
the
modules
branch
2 times, most recently
from
July 22, 2026 23:42
cd62200 to
7af4934
Compare
Encapsulated game modules live at modules/<name>/ with opinionated subdirectories (widgets/, rml_widgets/, gadgets/, actions/, policies/). module_handler.lua provides discovery, an include-once cache, contracts (manifest requires/provides), action/policy auto-registration with schema validation (shape shared with luarules/mission_api and PR #8226), and first-result-wins policy evaluation. policy_builder.lua is the modder-facing fluent layer that emits the same descriptors. Gadget/widget handlers scan module subdirectories game-side until the engine loads them natively (Recoil RFC). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated by BAR-Devtools scripts/sharing-module/generate.sh from move_map.tsv: git mv + quote-anchored include-path rewrites, plus the .busted ROOT addition for module-local specs. No logic changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The encapsulated module format at root-level modules/: auto-loaded subdirectories, module.lua manifests with requires-dependencies and per-state contracts (explicit manifest partition, implicit resolution), registration-style actions and pipelines with filename identity, and framework-shared types in root types/. Policy code lives with the policy, validation with the action; plain VFS.Include everywhere — no game-side include cache. The economy module owns the team resource snapshot; sharing's policies/actions move to descriptor form on the pipeline DSL. Squashed from the incremental format slices for a linear history the bar_editor chain stacks onto. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
Collaborator
Author
|
Superseded: the module framework now lands with the mission api stack (#8424), and the sharing content — expressed through the mode grammar, folded into modules/sharing — continues as the sharing v2 PR on top of #8462. (This PR's base is locked by the stack widget, so it is re-minted rather than retargeted.) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🧩 The Modules Format — tip of the sharing stack (#8411)
Note
Based on
sharing/05-game-modes-export(#8095) — the top of the sharing split — so the diff below is exactly this PR's own work: the overlay commits frommodules: module framework + loader hooksonward. The sharing feature itself is reviewed in the sharing stack PRs; this PR reviews the module format.Important
This branch is deterministically regenerated, same discipline as the sharing split: the ~80-file relocation commit is generated from a move map against
sharing_tab's tip, and the hand-authored overlay commits are cherry-picked on top — so when the sharing stack moves under it,just bar::sharing-module rebuild && verifyreplays the branch and conflicts can only appear in the small overlay, never the move commit. Regeneration is byte-identical (git rev-parse HEAD^{tree}equal before/after). Tooling lives besidebar::sharing-splitin BAR-Devtools and may fold into it.My [no LLM editor] Description
Intro
So you find yourself needing to encapsulate state gameside in an expressive way that isn't fighting other hook-based gadget architectures for supremacy. You want to put all of your modifications to game behavior in one place so it's easy to understand and discover. How do you organize this? I had a modular factoring of Sharing that I had originally described then coded for sharing_tab like a year ago, then saw what CampaignAPI was dealing with and did a Leo pointing at the TV when I saw what they were up to.
Background Recap
This section is basically a quick recap of Game Controllers & Policies, so if you've read that already, skip it.
Capabilities and concepts this branch leverages from upstream branches worth understanding before you dive into this:
Going to copy a mermaid diagram I stole from my deeper dive of these topics over in Transfer Library and change it to include modules:
flowchart TD Engine[Engine] subgraph Synced subgraph SL["Internal Service Layer/Module"] direction TB Controller["behavior_controller<br/>(game_unit_transfer_controller, …)<br/>executes commands within<br/>bounds set by PolicyResult"] Context["Context<br/>(cached)"] Policy[Policy] Result[PolicyResult] Controller --> Context --> Policy --> Result Result -.->|bounds execution| Controller end Gadgets["External gadgets"] end subgraph Unsynced UI[UI] end Command["«command»<br/>GG.* action request<br/>(independent data type)"] classDef iface fill:none,stroke:#888,stroke-width:2px,stroke-dasharray:6 4; class Command iface Engine --> Controller Result -->|published cache| Gadgets Result -->|published cache| UI Gadgets -.->|send| Command UI -.->|send| Command Command -.->|request| ControllerModules
Ok, so let's talk about this.
leaning heavily on the type system for correctness between files
has exactly one way to do something
that exactly one way is explicit and typed
module.luaacts as a manifest, think "package.json" in node appsapi.luamakes public an API (bags of methods) to shared (synced and unsynced) contexts, this is equivalent to "index.ts" in node apps:api_unsynced.luadoes the same for the unsynced context:not redundant - namespaces and file names don't repeat themselves. Files are relevant to their directory peers.
modules/sharing/actions/unit_transfer.luahas 0 ambiguityindividual files are concise
Policies
Policies constrain runtime behavior and drive the UI.
For example,
sharing/policies/unit.luaDeclaration order is evaluation order, first result wins, Compute always answers - the file registers its pipeline and returns nothing, and the filename is the category. A policy can insert its own gate anywhere in the order as long as it conforms.
Actions
Writing an action file is like writing a widget. You define your functions, you register them, you return nothing.
sharing/actions/unit_transfer.lua. Notice how the ctx is injecting useful data from our pipeline into our function.ctx(short for context, sorry -- I like brevity in lexical scoped variables) provides module-scoped data primitives. But the framework itself provides a baseline primitive to inherrit from. For exampleUnitTransferContext inherits from PolicyActionContext,ResourcePolicyResult inherits from PolicyResult, and so on. In this way we can explicitly classify overlap between modules.Domain Namespacing
mod options get broken up
One of the big wins here is modoptions get split up,
modules/sharing/modoptions.luais now a thing, root modoptions still returns one flat list - it just assembles it from module fragments. One of @WatchTheFort's biggest fears is mod option bloat, so we move them all to individual modules that need them. This should also benefit the packageability of mods.modes too
Currently, the only
ModeCategorythat exists (i.e. the Sharing Tab's top-of-tab mode dropdown is justmodes where category=Sharing). So it makes a lot of sense to move those same modes to the module that owns them atmodules/sharing/modes/.specs
Having the specs live in the module just makes sense. Put them next to the code they're working.
modules can be back-ported
Once you have this thing self-contained like this, it's easy to rip these things back out to the engine as exemplars.
Campaign API
Campaign API is currently an eventing system plus a behavioral override sidecar. The events and scheduler could live as a module that works the same as every other module then we could flip it on or off. But importantly, instead of a sidecar implementation, I'm arguing that we should refactor those behavioral subsystems and make them expose their own internal state as an easy to configure API, via policies or whatever makes sense for their factoring.
Conclusion
Whew. Sorry. It's a lot. But I do think THIS PR is not boring. This one has a lot of great ideas in it that kind of become apparent (to me anyway) under this organizational structure. Hopefully with ideas from both CampaignAPI and this branch, we can make expressing game behavior considerably easier over time.
Summary (LLM-generated, claude-fable-5)
Gives the sharing stack its final form: an encapsulated module format at root-level
modules/, with opinionated auto-loaded subdirectories, manifests + per-state contracts between modules, and the policy/action patterns from #8412 promoted to framework.modules/module_handler.lua: module discovery (module.luamanifests withrequires/provides, validated with loud errors), contract resolution (ModuleHandler.Get(name)), and registration-style loading foractions/andpolicies/— files callActions.RegisterValidate/RegisterExecuteor end pipelines with:Register()via loader-injected registrars (the widget-handler idiom: explicit named calls on an explicit local) and return nothing; identity is the filename; the runtime descriptor is assembled by the loader, never hand-authored. No hand-written parameter schemas — controllers are statically typed; a derived-from-LuaCATS schema returns when a data-driven dispatcher (mission_api/CampaignAPI) creates a boundary the type checker can't see.provides = { shared, synced, unsynced }andGet()merges shared + the current Lua state into one flat api — a widget never sees synced-only keys and vice versa; wrong-state access is nil at the first index. Contracts are plain eager tables (no lazy metatables).modules/*/gadgets|widgets|rml_widgetsgame-side — the shim for engine-native module loading (Recoil RFC to follow); the layout is the contract.modules/sharing/as a domain-scoped tree (resource/,unit/,take/,tech/,policy_views/, root-levelenums.lua/helpers.lua— the module name is the namespace, nolib/). Mode presets ship with the module (modules/sharing/modes/), and the module owns its lobby options viamodules/sharing/modoptions.lua, aggregated by rootmodoptions.lua.policies/resource.lua/policies/unit.luaexpress The Sharing Tab & The Modules Format — feature tracking (stack #8411) #8412's exact gate→compute control flow through a fluent builder (Policies.Pipeline():Gate(...):Compute(...):Register()) emitting plain descriptor lists — declaration order is evaluation order, pure functions only, category = filename. Decision code lives with the policy layer (policy_evaluation.luaholds both the live pipelines and the cached-factor rebuilds); unit validation is the action's declaredvalidateprecondition;shared.luafiles are serialization + transfer math only.modules/economy/(waterfill solver, share stats, manual share ledger, the team resource snapshot) as the first cross-module dependency: sharing declaresrequires = {"economy"}and consumes it via contract; the solver is tax-agnostic (sharing injects its tax resolver).sharing_tab's.🤖 Built with Claude Code (human-directed); every commit spec-verified as above.