Skip to content

Instantly share code, notes, and snippets.

@keithharvey
Last active November 7, 2025 18:53
Show Gist options
  • Select an option

  • Save keithharvey/a7d2375c0988e0ad7021002a3b8ad268 to your computer and use it in GitHub Desktop.

Select an option

Save keithharvey/a7d2375c0988e0ad7021002a3b8ad268 to your computer and use it in GitHub Desktop.
BAR Modules?
# BAR “Policies” Proposal — why modGadget isn’t quite the right factoring (but the idea is right)
**Problem we’re solving**
* We want radical configuration (game modes/behaviors) without spaghetti.
* Today, each gadget/widget decides things on its own; UI and gameplay drift apart; mod options are mostly one-shot.
**Goal**
* One clear place that *decides* gameplay rules.
* A single contract the UI can *read* and reflect.
* A predictable way for user actions to become *commands* in synced code.
---
## Core idea (modules, unidirectional)
Modules own a particular game behavior. Within modules, we can focus on making our code reusable by contributors in a way that is maintainable and reusable.
Reads and writes are different things:
* **UI (widgets)**: *read* policies and render; on explicit user actions they send a **command** up (no direct state changes).
* **Synced “domain modules” (gadgets)**: *own* the rules and execute commands; they *publish* policy results for the UI to read.
* **Mod options / game marketplace**: choose which modules/policies are active; they don’t mutate at runtime.
```text
UI (reads policies) ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─►
▲ (no direct writes)
│ user command (RegisterGlobal/SendToSynced)
Synced domain module (actions + policies) ──► publishes PolicyResults
```
This is essentially CQRS: **Commands** (writes) execute in one place; **Queries** (reads) are policy snapshots the UI consumes.
---
## Why not a single “modGadget” entry point?
Your modGadget concept (authorized messages → execute whitelisted functions) is a good *capability* but the wrong *home base* for long-term factoring:
* It re-introduces bi-directional, ad-hoc flows (hard to reason about, hard to test).
* It centralizes unrelated behavior, so surface area mushrooms over time.
* It hides the real seams (combat, team transfer, etc.) where contributors should work.
Keep the capability (validated commands from UI), but **locate it inside each domain module**, right next to its policies and actions. Small surfaces, clear ownership.
---
## Module shape (what contributors touch)
```
luarules/gadgets/
team_transfer/ -- a “domain”
actions/ -- Command handlers (writes)
resource_transfer.lua
unit_transfer.lua
policies/ -- Idempotent policy producers (reads)
unit_sharing_mode.lua
tax_resource_sharing.lua
callins/ -- Engine hooks this module owns exclusively
SyncedActionFallback.lua
```
* **actions/** = Commands (“do X”), single code path for effects.
* **policies/** = Pure/idempotent functions that compute `PolicyResult` structs for UI.
* **callins/** = the engine touchpoints the module *owns*; duplicates should be rejected loudly.
Your example: `team_transfer/policies/tax_resource_sharing.lua` publishes amounts, thresholds, tax rates, etc., which the UI can render without guessing.
---
## Contract the UI can depend on
Policies publish structured results (typed tables). Example fields the UI can read (from your policies):
* `canShare`, `sharingMode`, `allowTakeBypass`
* `amountSendable`, `amountReceivable`, `taxRate`, `remainingTaxFreeAllowance`, `resourceShareThreshold`, `cumulativeSent`, `overflowSliderEnabled`
> Swap policies at runtime; UI updates automatically. No logic in `gui_advplayerslist`.
---
## Tiny taste of the “command hook” API
When a command executes (e.g., a metal transfer), the module runs well-scoped hooks that can also update policy-relevant counters:
```lua
builder:RegisterPostMetalTransfer(function(transferResult, springRepo)
local cumKey = getCumulativeParam(SharedEnums.ResourceType.METAL)
local current = tonumber(springRepo:GetTeamRulesParam(transferResult.senderTeamId, cumKey)) or 0
springRepo:SetTeamRulesParam(transferResult.senderTeamId, cumKey, current + transferResult.sent)
end)
```
This keeps *effects* and *policy data* together, testable, and local to the domain.
---
## Social/process (brief)
* Let people build modules that are expressed via mod options, but keep them **as PRs against specific modules** (quality gates internally, shared patterns between modules).
* A small marketplace later is fine; the module API encourages reuse and better factoring.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment