Skip to content

Instantly share code, notes, and snippets.

@badosu
Last active October 27, 2025 23:27
Show Gist options
  • Select an option

  • Save badosu/ade5ec90d74bddea6cadff8b4d0dcc15 to your computer and use it in GitHub Desktop.

Select an option

Save badosu/ade5ec90d74bddea6cadff8b4d0dcc15 to your computer and use it in GitHub Desktop.
Badosus list of random Recoil/Bar things

BAR Widget Curation proposal

I probably have a more comprehensive suggestion somewhere, but I'll include here the one that is simplest to get something done with.

Discord message Ref.

Here's at least one attempt to soften the blow when the day comes to disable custom widgets by default:

  • Have a repo containing all "public" widgets
  • Every once in a while hash the widgets files, and store this list of hashes
  • Hardcode this at VFS.ZIP level
  • At VFS.ZIP level disable loading VFS.RAW files except those present in the hash list. Specifically be a bit smarter and apply this logic for widget loading at widget handler level
  • Enable modoptions to include hashes

This is the dumb, but non-apocalyptic solution, enforces BAR policy of only public widgets allowed and allows users to allow their custom widget for a particular game.

There are additional considerations, like for example disallowing VFS.Include to enable "hashed widget that loads unhashed widget" case but you got the gist

I'll use an example from BAR, BAR in this example in many places has code that does what you need on a higher level, this is not a problem in fact it's a good thing. I just want you to keep in mind this is not exactly what we'd do in terms of pure Recoil usage, the important bit is identifying the problem ("I need to send a notification something happened in synced code to UI") and what kind of things are involved in getting a solution.

  • BAR wants to notify UI that it received from engine that units were given from a team to another here

  • To do that, in the synced part of the gadget, it uses SendToUnsynced("NotificationEvent", "UnitsReceived", tostring(player)), I won't explain this usage, read the second link I sent you on the recoil engine guide. Or look for other places in BAR code where this is used.

  • Notice in the unsynced part of the gadget it does gadgetHandler:AddSyncAction("NotificationEvent", BroadcastEvent), this is basically boilerplate from BAR gadgetmanager to handle these messages automatically, but it's importante here to notice the pattern of synced sends a message -> receives still on gadget env but unsynced (could be handled in another gadget, but on the unsynced env)

  • On the handler it specified above BroadcastEvent it does: Script.LuaUI.NotificationEvent(event.." "..player). Basically it's telling now LuaUI (where widgets live) a message in the format event.." "..player (I guess "UnitsReceived "). What this does is to basically require some global NotificationEvent to exist in the LuaUI environment that will receive this message.

  • See that in sound notifications widget we do listen to NotificationEvent and handle it however we see fit (we still need to parse the string so the arguments can be used, e.g. decouple the event from the args <someevent> <someargs>)

  • Maybe that widget does something with UnitsReceived event or not, what matters is that we finally figured out how to make a widget to receive a synced environment message.

So in very broad conceptual terms:

  • There exists synced/unsynced luarules, and luaui (luaui is always unsynced)
  • If we need to send a message from synced to luaui first we send the message from luarules synced to luarules unsynced with sendtounsynced
  • Then if we still want to send from luarules unsynced to luaui:
    • Use Script.LuaUI.X in luarules unsynced and define a global function called X that handles this message in LuaUI.
    • Use SendLuaUIMsg in unsynced luarules and implement function widget:RecvLuaMsg(msg)

You might not need to send to LuaUI, you can do it in luarules unsynced (the unsynced part of gadgets), in fact many places in BAR do so, it's a matter of what exactly you want to achieve (solve a problem LuaUI needs to be unaware of or not).

Again, might be a little bit hard to read, but all this is kinda explained in this article its just not easy to figure this out without some context (and basic knowledge on what synced/unsynced luarules/luaui are and what they are concerned with)

Memoized Include

Recoil lua environments have filesystem IO sandboxed. As such there is no require in Recoil vanilla lua.

Instead, gamedevs are encouraged to use VFS1 instead.

A common pattern is to just VFS.Include everything the gamedev needs, but it incurs in space and time overhead: the engine has to access its filesystem layer every time, and for persistent stacks (such as gadgets and widgets main scope) a different allocation is performed for each identicall VFS.Include call.

Idea

Implement some sort of "memoized include", an include that caches and retrieves identical invocations.

Basecontent has an example of such, it is already used by games that don't override LuaIntro for example - or any environment that uses LuaHandler for what its worth.

Drawbacks

  • Gamedevs must be aware of the distinction between VFS.Include and a require in terms of behavior
  • An unattentive gamedev might use require in a circumstance there is a requirement to execute the code in the file instead of just the result.
  • As a concrete example, toggling a widget might not refresh state

Effort

  • Basecontent require does not return the result of the file execution, this is required for emulating the pattern commonly used to define modules in lua, e.g.:
-- mymod.lua
local M = {}

function M.greet()
  Spring.Echo("Ohai")
end

return M

-- outside scope

local MyMod = require("mymod.lua")
MyMod.greet()
  • As mentioned before, hot reloading is affected, this can be worked around. One option is having a "devmode" switch, but there are other ways, this is not a big deal in LuaRules environment, mostly LuaUI.

Footnotes

  1. Read more about VFS in this guide

A prototypical mouse trigger widget for enabling mouse bindings.

Better used in conjunction with chain_actions widget to ensure we are able to halt the chain on engine actions.

bind <key> mouse_trigger [left|right|mmb|mouse{4...}] | <action>
// example
bind Any+ctrl mouse_trigger left | chain force select ...
bind Any+ctrl mouse_trigger left | game_action_does_not_need_chain

https://gist.github.com/badosu/13b68ffe460aff233ce735e0817dd58b

A "quick and dirty", tinker with the engine:

  • Create a folder you'll use as your "recoil/spring directory", we'll call this folder <my-recoil-dir>
  • Inside that folder, create maps and games directory.
  • Download the engine here and extract somewhere
  • In the extracted engine directory, run ./spring --isolation --write-dir <my-recoil-dir>
  • Change the vroom code and see your changes in the game (note reloading changes in game is limited)

A few resources:

  • Spring Wiki: A more comprehensive documentation, but outdated
  • Recoil Site: More up-to-date, less comprehensive

What you'll need is to have a folder to contain the assets for games and maps, the one you set with SPRING_{DATA,WRITE}DIR.

Create the folder, then create a maps and games folder inside it. Inside maps copy paste a map, you can use one from your BAR installation if you have one.

Inside games you can clone a game and call it <GAME>.sdd e.g. BAR.sdd. You can also symlink with that name to a clone in another locations.

This should enable you to run the spring binary directly, like this: ./spring --isolation --write-dir <FOLDER> (adjust for windows specific stuff).

-- pseudocode-ish
-- just for educational purposes, does not work as-is
function widget:GetInfo()
return {
handler = true,
}
end
local snoozableCallins = { "Update" } -- DON'T INCLUDE THE CALLIN THAT IS SUPPOSED TO WAKE UP!
local function Sleep()
for _, addon in handler.addons:iter() do
for callin in snoozableCallins:
handler:RemoveAddonCallIn(callin, addon)
end
local function WakeUp()
for _, addon in handler.addons:iter() do
for callin in snoozableCallins:
handler:RemoveAddonCallIn(callin, addon)
end
local function MaybeWakeup(msg)
if msg == "lobbyOverlay:WakeUp" then
WakeUp()
return true
end
end
local function MaybeSleep(msg)
if msg == "lobbyOverlay:Sleep" then
Sleep()
return true
end
end
function widget:RecvLuaMsg(msg)
MaybeWakeUp(msg) or MaybeSleep(msg)
end

Use manager utilities!

Gamedevs often implement some solution, replicating a pattern already seen, that works for the intended goal but leads to repetition and a codebase that is more bug-prone, highly coupled to ad-hoc behavior and difficult to reason about and maintain.

It is reasonable that this happens often, as Recoil strays far from regular vanilla Lua, especially with regard to the different ways the lua environment can be affected - and more so in upget1 code - which might make gamedevs hesitant to employ patterns they would otherwise in any other language.

The basecontent-derived upget handlers offer many tools to deal with this situation, and they have been around for decades! But they are often treated as an arcane source, not understood and very rarely adapted to a games needs.

Case study

Settings Widget

Often gamedevs use globals to talk back and forth between upgets, let's imagine a scenario.

  • A settings manager widget2. This widget has knowledge of, can write and read each widgets setting state.
  • The settings manager offers an api and requires an api for each widget and each setting.

In the wild, it might look something like this (WG is the shared global in this instance):

-----
-- gui_menu.lua

local showPrices
local function setShowPrices(value)
  showPrices = value
  -- Code that refreshes internal state
end

function upget:Initialize()
  WG['menu'] = {}
  WG['menu'].setShowPrices = setShowPrices
  -- we omit the getter pattern for brevity
end

-----
-- gui_options.lua

-- we omit the gui code for brevity
local function clickedOnShowPriceDropDown(value)
  if WG['menu'] and WG['menu'].setShowPrices then
    WG['menu'].setShowPrices(value)
    WG['someotherwidget'].showPricesChanged(value) -- another widget that cares about this setting
  end
end

That's a lot of boilerplate! Even considering the case that we can 'metaprogram' a bit to reduce repetition, the setters and getters still need to be defined. Further attempts to improve this situation while not rethinking the architecture only lead to forever more clever, ad-hoc, bug-prone solutions.

Idea

Use handler:RegisterGlobal to define shared APIs and sculpt the global scope of your widgets.

-----
-- gui_options.lua

-- some API for dealing with options, containing cache and event dispatch
-- e.g. set, get, register and so on
local optionsStore
local setShowPrices

function widget:Initialize()
  widgetHandler:RegisterGlobal('Option', optionsStore.register)
  setShowPrices = Option('showPrices')
end

local function clickedOnShowPriceDropDown(value)
  setShowPrices(value) -- calls listening widgets
end

-----
-- gui_menu.lua

local setShowPrices, showPrices
local function showPricesChanged(value)
  showPrices = value
  -- Code that refreshes internal state
end

function upget:Initialize()
  -- Option returns current value and setter
  setShowPrices, showPrices = Option('showPrices', showPricesChanged)
end

local function widgetDecidesToSetOption(value)
  setShowPrices(value) -- calls showPricesChanged
end

The proposal omits many important considerations such as: ownership, sandboxing, setting without callback (for hot loops or special internal logic), etc...

What's important is that such considerations don't exist if all widgets use the shared global anyway, and now we don't depend on whether some widget changed names or changing both options and menu widget, having to gather all the context embedded in both codebases and increasing the chance of introducing regressions or spaghettifying more.

More

Use handler = true for raw access to the handler itself, enabling further customization. An upget with handler = true can manage other widgets and define custom callins for example, encapsulating this logic and insulating the manager code from containing game specific logic.

Footnotes

  1. upget and addon are architecturally the same thing, the difference lies in the legacy: upget for widgets and gadgets and addon for the unified LuaHandler. Extending the analogy further: widgets are LuaUI addons and gadgets are LuaRules synced and unsynced addons.

  2. For the sake of the argument we are dealing with this architecture for some (probably legacy) reason, instead of a more elegant solution.

Widget ramblings

In general, widgets are just a particular name for the same pattern that exists in many places in Recoil lua programming, which are code pieces encapsulating the responsibility for a given feature wired up to callins. In this sense one good practice is to try and compartmentalize one widgets responsibilities as much as possible. In BAR it's common to share common code via widget globals manipulation as another widget, this is a legitimate pattern, but overused and problematic when:

  • For stateless functionality it should just be a library. Cases like this are helper functions and modules that don't need a shared state between widgets.
  • For stateful functionality common widgets provide a modular functionality that might make sense, but there should in many cases a stateful include would make more sense. In this case a deeper discussion would be necessary to discuss the pros and cons of each, but safe to say a common widget would make sense where a "library as server" architecture makes sense or shared callins should be concentrated in a single widget instead of a "macro" approach (the library metaprograms callins into the callee module).

There are other motivations such as portability and maintainability but we'll not expand too much.

Input actions in this context

Widgets might want to listen to mouse and keyboard interactions, this happens via Mouse{Press,Release,Wheel}, Key{Press,Release} and Text{Input,Editing}.

The Action system

For keyboard handling, and assuming the functionalities we care about don't require explicit and raw Key{Press,Release} usage widgets are encouraged to use the Action system.

This concept is native to the engine, but many Lua handlers also implement it locally via the actions module.

In particular we will concern ourselves to press and release actions, which map to the keypress and keyrelease callins. In this case a dispatch system that is relayed to when a widget didn't explicitly capture Key{Press,Release}.

The basic shape of it is:

Action overview

Games or players can bind keysets to actions, e.g.:

bind shift+c say something here

Widgets or (unsynced gadgets) can listen to actions:

local function sayActionHandler(actionName, rawArg, args, data, isRepeat, isRelease, actions)
  actionName == "say"
  rawArg == "something here"
  args == {"something", "here"}
  data == { hue = "br" } -- curried data on register
  type(isRepeat) == boolean
  type(isRelease) == boolean
  -- actions is a list of complex objects representing all actions that were triggered in this keyaction

  local haltChain = true

  Spring.Echo(actionName .. rawArg)

  return haltChain -- when true does not allow subsequent actions to be handled
end
local types = 'p' -- t: text, p: press, r: release, R: repeat
local data = { hue = "br" }
widgetHandler:AddAction("say", sayActionHandler, data, 'p')

Managing code complexity and performance on unsynced addons (widgets, unsynced gadgets)

The core architectural concern of an unsynced addon to be performant is decreasing the overhead for managing state while also not offloading non-gpu compute to graphical callins.

So the main design concerns tied to performance, to what relates specifically to the concept of addons being encapsulated callin dispatchers:

  1. Cache state, keep state minimal, use entity/game lifecycle callins to minimize overhead when managing state. Obvious example: instead of iterating on all units every update/gameframe, register in cache with unit created callins, unregister from cache on unit destroyed callins.
  2. Decouple state management from graphical load. Avoid computing state during graphical callins, precompute state beforehand for the more intensive or complex operations.

Important to note performance is not the only concern here, code complexity is much easier to tame when concerns are split in manageable pieces.

A short and incomplete description of gamestate/update use cases

An important distinction must be made here between update/gameframe use cases:

  • gameframe: must be used when your widget requires frame perfect sync with game state. when frame perfect sync is not required, can still be throttled (e.g. gf % n).
  • update: must be used when your widget is related to unsynced concepts, e.g. chat, UI, etc. can be used to sync game state when the featureset of the addon does not require frame perfect sync, e.g. some statistics aggregator. its important to keep in mind that the throttling is heavily impact by fps, as opposed to gameframe.

Remarks

Managing state in update/gameframe can be faster than lifecycle management

Depending on the runtime characteristics of the addon, relating to its featureset, its more performant to batch operations on update/gameframe.

One easy case is when the sample size of the entities managed is much smaller than the collection of entities that will get passed through lifecycle callins.

Case study: a widget only concerns itself with units of a particular unitdefid, and a player can have at most 5 of them during the game. You can just invoke a query for getting all units from a player with a particular unitdefid during gameframe (if gamestate sync required) or update (if unsynced feature). The overhead of the lifecycle callin being invoked for every unit for this widget when it only cares for managing at most 5 units makes it obvious its not the best option.

Small sample size overhead from lifecycle callins can be mitigated by the addon manager allowing addons to register callins they want to receive. e.g.:

widget.WantedUnitDefId = [1, 2]

-- only gets called for unitdefids 1 and 2
function widget:UnitCreated()
end

Engine does not support certain entity/game lifecycle callin

Forcing us hooking to update/gameframe.

For example:

  • A keybind has been changed by the player. example
  • The current active command has been changed by the player. example
  • The buildprogress for a unit being built has been updated. example
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment