Skip to content

Policy-Based Team Transfer Framework#7

Open
keithharvey wants to merge 8 commits intosharing_tabfrom
pipeline
Open

Policy-Based Team Transfer Framework#7
keithharvey wants to merge 8 commits intosharing_tabfrom
pipeline

Conversation

@keithharvey
Copy link
Copy Markdown
Owner

@keithharvey keithharvey commented Aug 20, 2025

Technical Goals

The purpose is a self-contained team_transfer/ module that cuts off its own API from the engine and state. That last bit is important because it allows the API to talk explicitly about the plan and not the execution.

This moves execution to one place, which is convenient because that serves as our one bit of code that bridges back over to the engine. So as we online new engine capabilities like /take, we have a place to put it.

Future Goals

The goal here would be to gradually take over our own behavior entirely, centralize the SyncedActionFallback directly related to sharing within this module, and have all other game modules talk to us for anything sharing related. This greatly simplifies our ability to reason about a comprehensive solution to sharing in the long run. And it would provide an example LUA implementation that other games could use if the engine ever wanted to get out of the deciding who goes where when. That would be extremely clean. Engine is just raw hooks to explicitly do something now. Games listen, for example, to player abandoned, then tell the engine what to do. The game does its thing, but the rules around that behavior live entirely in team transfer.

Implementation Overview

  • core module files implementing the pipeline architecture
  • test files with unit and integration coverage
  • Fluent Policy API: policy.ForAlliedCommands.WhenGuard.Allow()
  • Centralized Pipeline: Evaluates policies and handles legacy callbacks.
  • 5 Policy Implementations (so far): unit sharing modes, resource tax, ally assist, enemy transfer, system cleanup
  • Complete Type System: this was a pain but does seem to work well now that it's in place.

API Examples

The fluent API enables readable policy definitions:

-- Unit sharing mode policy
policy.ForAlliedUnitTransfers.Use(function(ctx)
    if not sharing.isT2ConstructorDef(UnitDefs[ctx.unitDefID]) then
        return { deny = true }
    end
    return { allow = true }
end)

-- Resource tax policy  
policy.ForAlliedResourceTransfers.Use(function(ctx)
    return {
        applyTransfer = { sent = breakdown.actualSent, received = breakdown.actualReceived },
        expose = { taxRate = taxRate, threshold = metalThreshold }
    }
end)

-- Command policies with fluent chaining
policy.ForAlliedCommands.WhenGuard.Deny()
policy.ForEnemyCommands.WhenReclaim.Allow()

This was a personal stylistic choice we could definitely convert to something purely functional or whatever pattern we want to expose our API in.

Architecture Benefits

  • Single Source of Truth: All transfer logic centralized in one module
  • Extensible: New policies can be added without touching core engine code
  • Testable: Comprehensive unit testing of business logic separate from Spring API
  • Type Safe: Full TypeScript-style definitions prevent API misuse
  • Future Ready: Clean abstraction layer for new engine capabilities like /take

Legacy System Integration

  • Backward Compatible: Existing gadgets continue working through legacy pipeline callbacks
  • Gradual Migration: Old AllowResourceTransfer/AllowUnitTransfer hooks still supported
  • Centralized Execution: All transfer logic now flows through single pipeline in game_team_transfer.lua
  • Policy Loading: Auto-discovers and loads all policies from team_transfer/policies/ directory

Why So Big?

Unfortunately this PR needs to do a couple of novel things at once. It is unit testing, that's a significant part of that LOC, and it's pulling in engine hooks and adding a pipeline system to abstract them for the rest of the module. That code, to me, is mostly boilerplate to accomplish the goals. In most languages it would be generics and library code, and maybe we could do that in LUA - but it seemed ambitious to just move all of this directly into common and try to genericize it before we even had implementation number 1. That would not make this PR less complicated, because we're still pulling in the entire team transfer system and engine calls, so we need some boilerplate for that.

Testing Status

  • ⚠️ Unit Tests: test files covering predicates, policy builder, resource calculations. They seem kind of garbage I'm going through them one at a time and making them suck less.
  • ⚠️ Integration Tests: Full pipeline testing with mock Spring API calls
  • ⚠️ Policy Tests: Validation of unit sharing modes, tax calculations, ally assist
  • ⚠️ Game Testing: Basic functionality verified, needs extensive multiplayer testing
  • 📋 Test Runner: New README_test_runner.md with comprehensive testing framework

These are the least finished part of this. Working on that now.

Game Testing

Works but needs massive amounts more testing.

Remaining Work

  • Geo/Mex Upgrades: Integrate with existing upgrade transfer logic
  • Resource Sharing Buildings: Implement Chronopolize's building-based sharing example
  • Abandon/Cleanup: Verify existing abandon behavior still works correctly
  • Multiplayer Testing: Extensive testing across different game modes and team configurations
  • Performance Testing: Validate pipeline performance with large unit counts

@keithharvey keithharvey changed the title Pipeline Carebear Pipeline Aug 20, 2025
@keithharvey keithharvey changed the title Carebear Pipeline Team Transfer Policy Pipeline Aug 20, 2025
@keithharvey keithharvey force-pushed the pipeline branch 3 times, most recently from 9b27e77 to 12be0ad Compare August 22, 2025 21:23
- Refactor unit sharing system into three modes: enabled, t2cons, disabled
- Fix /take command blockage when sharing is restricted
- Decouple Unit Market transfers from sharing restrictions
- Remove redundant unit transfer check from tax resource sharing
- Messaging when transfers are incomplete
- common unit sharing logic goes in common, with some limited intellisense support
- Add isEconomicUnitDef function to support "combat" and identify only economic units (factories, assist units, energy/metal buildings)
- ui changes to support this, including greying the transfer button (with a tooltip) in the advanced player list.

Addresses Issue beyond-all-reason#4416 and incorporates PR feedback.
- Add modoption `player_metal_send_threshold` to defer metal tax until a sender’s cumulative sent metal exceeds the threshold; energy unchanged (taxed only by `tax_resource_sharing_amount`).
- Track cumulative sent metal per sender and expose via team rules params: `metal_share_cumulative_sent`, `metal_share_threshold`, `resource_share_tax_rate`.
- UI (AdvPlayersList): while dragging the share slider
  - Energy shows received/sent preview.
  - Metal shows amount_to_send/max_allowance and caps the slider by min(my metal, receiver free, remaining allowance).
  - Right-justified label with auto-sized grey background to fit two-number display.
  - Echo on send uses i18n: “Sent Xm[/Ym allowed by the player send threshold]” (optional bracket when threshold > 0).
- Refactor: add `common/luaUtilities/resource_share_tax.lua` and use it from both gadget and UI to keep the math in one place.
- Safety: clamps against receiver max share, handles 100% tax case, guards against negatives. Default threshold is 0 (immediate tax if base rate > 0). No changes to unit-sharing logic.
- i18n: add `ui.playersList.chat.sentMetalSimple` and `ui.playersList.chat.sentMetalThreshold` (en).
keithharvey and others added 2 commits August 24, 2025 00:36
Introduces mode-first sharing configuration where UI modes set/lock/hide
individual modoptions. Enables composable sharing policies with clear
separation between UI orchestration and gadget enforcement.

- Tag sharing modoptions with sharing_category for UI grouping
- Add sharingoptions.json with 4 modes: no_sharing, limited_sharing, enabled, customize
- Include JSON schema + CI validation for mode configuration
- Support allowRanked flag to disable ranked queue for experimental modes
- ui and sorting issues handled with the `depends_on=parent` flag on modoptions.lua always putting that option after its parent
- passes the mode onto the game through a system mod option `_sharing_mode_selected`. This required giving the game a little knowledge of modes, but it owns them anyway and it is limited to a single helper file
* ui and sorting issues
* disable mod options entirely based on mode whitelist. This required giving the game a little knowledge of modes, but it owns them anyway and it is limited to a single helper file
@keithharvey keithharvey changed the title Team Transfer Policy Pipeline Team Transfer Policies Aug 24, 2025
@keithharvey keithharvey changed the title Team Transfer Policies Policy-Based Team Transfer Framework Aug 25, 2025
The share slider was rendering behind other UI elements because commit 064e1ee
changed the widget layer from -4 to 1. Lower layer numbers render first (behind)
and higher layer numbers render later (on top). Reverting to layer -4 fixes the
z-index issue while preserving the team transfer API improvements.

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Keith Harvey <keithdanielharvey@gmail.com>
@keithharvey
Copy link
Copy Markdown
Owner Author

keithharvey commented Sep 18, 2025

The problem

User Story

As a lobby host

I want to be able to configure a fun group of mod options to enable the game to be played with sharing options restricted.

As a dev

D1) I want to be able to online new rules quickly
D2) I don't want to have knowledge of other rules when I write my own
D3) I want to be able to define new groups of rules easily
D4) I want type support when describing rules
D5) I want to be able to unit test my rules
D6) I want to be able to, at runtime, modify sharing rules. For example whenever a particular building (energy storage?) is built, I can enable arbitrary categories of transfer

Discussion of Technical Limitations of Status Quo

Stateful Gadgets

Gadgets in BAR are little emperors, so if you add a gadget that effects unit transfers, you need to know about every other gadget that effects unit transfers. This leads to a proliferation of sync calls between gadgets as system complexity grows

Runtime Game Rules

If you accept (D6) as a requirement, then think about the following example:

gui_advplayerslist needs to draw the metal sharing button. It needs to know

  • current metal
  • is sharing enabled/disabled by mod options -- this would "disable" the button"
  • reason it was disabled for a tooltip

Then they click into that and it now needs to know

  • max storage
  • max sendable amount
  • current metal
  • [modoption] tax rate (np get it from team rules params - but suddenly I have tax rate logic in gui_advplayerslist)
  • [modoption] metal threshold logic

Helpers get you around this to some extent by just having the UI layer call helpers after parsing state consistently, but whenever you want to express realtime game rules, this logic, and the spider web of state, becomes onerous to track down for every caller into a functional helper.

Complexity Limits

This is somewhat secondary, but the current system really CANNOT support internal complexity outside the engine, because it is interwoven with the engine inherently with the gadget system.

The old tax_resource_sharing gadget, for example, relied on AllowResourceTransfer from the engine directly. So our game code calls out to the engine to Initiate a transfer, we intercept the engine validation hook, evaluate it against our gadget logic, stop the transfer, then reinitialize a transfer through the engine. The psychological load of this pattern on developers is enormous. You not only have to know intimately every other gadget that is listening to AllowResourceTransfer, but you have to know they're going to respect your state flags when your re-transfer comes back around to AllowX from the engine the second time. We control the game code. There is no reason to talk to the engine about transferring units between teams when that logic lives with the game. This same pattern repeats verbatim with AllowUnitTransfer.

Unit testing this in isolation of the engine becomes problematic. There is no way to isolate state internal to a game module in useful ways if you have to go through these black boxed spring hooks you fired off yourself.

The Engine Wants Out Anyway

The engine team has already signalled they want to get out of dealing with the "transfer reason" passthrough for AllowUnitTransfer. It is currently a "capture" boolean on AllowUnitTransfer, which is WOEFULLY inadequate for trying to figure out who called TransferUnit from data during the round-trip (so again, you're back to stateful flags between gadgets). Better would just be to call our game module and have callers conform to our internal API.

Ideal Replacement System Design Goals

  • rules should be composable and isolated
  • rules should be easy to understand
  • rules should have predictable, uniform outcomes in behavior
  • clear type, supported API boundaries with the other game gadgets and the UI
  • unit testable
  • integration testable

Technical Solution

Introduce a new module to own team transfers.

Sharing Options

Sharing options are a single dropdown under the Sharing tab that allows developers to lock/hide/show specific mod options for a given 'sharing mode'.

[Screenshot of Sharing Tab]

Team Transfer Gadget API

The outside API exposes things that mirror the current Spring API, but internally we build a list of policies that we create a cache derived current game state for every combination of my_team -> ally_team. This gives a number of advantages:

  • UI boundaries for the "current context" are cached every N game frames
  • policies defining that cache are composable because they are isolated from state because they are internal to the module
  • introduces the extremely useful action as a first class concept in the game (e.g. Transfer Units, Transfer Resources, Take Player). This is a new capability with this module/policy pattern because we do NOT have to roundtrip to the engine to effect work. We update the game code to talk to our new module as a single source of truth for certain game actions related to team transfer.

Policies

Policies are a way to define dynamic, cacheable game rules in bar. This same pattern could be similarly replicated for every game rule we have but I think team transfer is a good domain to start, because it is in need of the capabilities this sort of factoring offers.

[team_transfer/api_gadgets]

[initialize] > evaluate many [policies] present in current sharing option
--------------> caches [policy result] (UnitTransferPolicyResult, ResourceTransferPolicyResult)
[actions] > transferUnits(senderTeamId, receiverTeamId, unitIds)
--------------> [actions.transfer_units(actionContext, which includes unitTransferPolicyResult)]
--------------> returns [TransferUnitResult.units_sent,block_message]

[Team Transfer Architecture Diagram]

Case Study demonstrating how to build building_unlocks_sharing.lua

@keithharvey keithharvey force-pushed the sharing_tab branch 3 times, most recently from 8400bc2 to f3fbabc Compare October 27, 2025 20:07
@keithharvey keithharvey force-pushed the sharing_tab branch 3 times, most recently from 6e22288 to 2639ae9 Compare November 8, 2025 02:06
@keithharvey keithharvey force-pushed the sharing_tab branch 2 times, most recently from a058b07 to b93a8e4 Compare December 6, 2025 08:05
@keithharvey keithharvey force-pushed the sharing_tab branch 6 times, most recently from 2fa8883 to ff0fa4f Compare December 21, 2025 02:26
@keithharvey keithharvey force-pushed the sharing_tab branch 3 times, most recently from 4affe98 to aa3e22d Compare January 5, 2026 21:04
@keithharvey keithharvey force-pushed the sharing_tab branch 13 times, most recently from 0a3d006 to ce774d4 Compare March 15, 2026 00:10
@keithharvey keithharvey force-pushed the sharing_tab branch 2 times, most recently from 5b33353 to 8cb6740 Compare March 19, 2026 22:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant