Better Belt Balancer


Belt balancers that click together into arbitrary shapes. Highly performant; megabase-ready (UPS hit is ~equal to vanilla hand-crafted belt balancers).

Utilities
6 hours ago
2.0 - 2.1
982
Logistics

g Performance?

a month ago

The description claims that this mode doesn't do per-tick Lua logic. This is good, because I know first-hand just how awful Belt Balancers can be. But if not on-tick Lua logic, then how are you moving items about? Are you hooking into native functions of some sort?

a month ago
(updated a month ago)

BBB constructs "real" belt balancers out of belts/splitters and puts them on a hidden surface, then links the inputs and outputs of the balancers to the corresponding connection points on the belt balancer parts placed in the "real world".

Thus, the mod only executes Lua when adding/removing balancer parts (or connections to parts). When the balancers are not being modified, no Lua is executing.

a month ago

Aha! OK, that's quite clever. So you're not moving items through Lua, you're letting native splitters do the work for you. OK, I like that :)

Reason I ask is I've been using Belt Loaders into Autolinked Chests for the sake of using native code, but I'm not sure what the constant loading and unloading of chests costs me. Thanks for making this.

a month ago

Okay, so... If the mod does not do per-tick logic...

Please explain why you, in your own source code, are subscribing to EventOnTick all over the place.

a month ago
(updated a month ago)

@glektarssza All those call sites you identified are in the obs subpackage, which is part of the test suite for BetterBeltBalancer. Each guest/go/obs/*/main.go is an observer mod — a helper mod that builds a test world, samples it on a tick schedule, and logs what it sees so the assertion scripts can grade the actual mod. An observer needs a clock by design.

None of those packages are part of the distributed BetterBeltBalancer mod. The test runner stages them as separate throwaway mods beside the one under test, and they're not in the release zip. The mod itself is guest/go/main.go and its siblings, which subscribe only to world-mutation events — built, mined, died, cloned, surface deleted, forces merged, undo/redo. You can verify from the actual released mod zip without trusting me: fk_api_gen.lua lists every event the compiled guest can reach, and on_tick isn't in it.

8 days ago

Fair point, @Techrocket9. I stand corrected about the EventOnTick references.

However...

From your code:

--   guest calls fk.defer() any number of times during tick T
--     -> one on_tick dispatcher is registered, once
--   tick T+1
--     -> the handler unregisters itself, Factorio stops calling in
--     -> fk_on_deferred() runs once, and the guest drains its own queue

Specifically, this comment is from runtime/lua/fk_mod.lua#L1176C1-L1180C73 in your own Techrocket9/FkLua repository. Which is a separate source repository from the mod repository. Made it very hard to figure out, honestly, that this is where the on_tick was happening...

You can verify my findings if you don't believe me by just download your own mod. Inside control.lua , on line 144, defer gets defined as a call to arm_deferred which is defined on line 1244. This function explicitly calls on_event(defines.events.on_tick, flush_deferred) on line 1252.

Following this thread into fk_module.lua, local IMPORTS is hooked in on line 2570 and pulls in defer on line 2575 via fk_import, storing it in F[4]. F[4] is then used in 14 other places in fk_module.lua alone (lines 18530, 27875, 27882, 33077, 39090, 39205, 39496, 44558, 44697, 44876, 45595, 46029, 46864, and 47223).

4 days ago
(updated 4 days ago)

@glektarssza Better Belt Balancer is written in Go and compiled to Lua by FkLua, a toolchain for writing Factorio mods in Go or Rust. control.lua is generated per mod from FkLua's runtime, which is why the design comment you quoted lives in that repository. What FkLua provides, among other things, is a way to run work on the next tick and to spread housekeeping across ticks without hitching. Factorio has no end-of-tick hook and no one-shot timer, so the only way to do either is to register a tick handler and take it down again. FkLua's rule is that a handler is registered only while work is owed and removed as the first statement of the handler itself.

What you traced is the FkLua runtime scheduling Better Belt Balancer's deferred work with a callback, and it does not change the "no per-tick per-item Lua logic" claim. Better Belt Balancer itself registers no tick handler. A mod built with FkLua gets a standing on_tick handler only by exporting fk_on_tick (registered by the branch at control.lua:2344) or fk_on_nth_tick (registered through script.on_nth_tick at line 1318). The export table is at the bottom of fk_module.lua, from line 92666; for Better Belt Balancer neither is in it. Better Belt Balancer subscribes to twenty-four events, listed in fk_api_gen.lua: entities built, mined, died, cloned, rotated and flipped, surfaces cleared and deleted, forces created and merged, undo and redo, and its own runtime setting changing. on_tick is not among them. A balancer compiles when you build it (or edit it/its belt connections) into a hidden network of real Factorio splitters and linked belts, and between edits the engine carries the items with no script involved.

Better Belt Balancer uses three features of the FkLua library that can temporarily register on_tick. The first is the defer you found. When a part is placed or mined, a belt is laid against a balancer, a blueprint is pasted, or a setting changes, the mod records what changed and asks for a flush. The FkLua runtime registers on_tick once, the handler fires on the next tick, unregisters itself at line 1202, and the mod recompiles what changed. The fourteen F[4] sites are the compiled form of thirteen requestFlush() calls in the Go source, and fk_module.lua names the enclosing function in a comment above each one. Ten are inside fk_on_event and its handlers: onPart, onNeighbour, onNetworkLoss, legacyBuilt, reapFastReplaced, noteInsertProbe, the setting-changed handler, and the collector check at 39496. Two are in rebuildFromWorld, which runs once when a save is opened by a build that did not write its heap. One is in flush, the deferred handler asking for a follow-up. One, at 18530, is in fk_on_call, which is the /bbb-audit console command. Every one of them is reached by an edit, a load, or a command someone typed. None are triggered by items moving.

The second is FkLua's garbage collector, which reclaims what an edit allocated in paced steps of bounded cost across a few ticks instead of one pause, and unregisters when the collection ends. The sites at 18530 and 39496 are its pacer, guarded by the 262144 on the line above each: once the guest has allocated 256 KiB since the last collection, the end of an event arms a pass of the garbage collector. Without an event, no memory is being allocated by Better Belt Balancer and the garbage collector will not be triggered.

The third is the memory pre-build, which fills ahead in bounded pieces while the guest heap is growing, which for Better Belt Balancer is rare: the collector holds the heap near half a megabyte. The run_after_load handler beside them is for an export this mod does not have and never registers.

Two of those can also be re-armed when you load a save file. Factorio does not save event registrations, so if a save is taken between an edit and its flush, or mid-collection, after_load re-arms from a flag in storage (lines 2167 and 2193) and finishes the work that was owed.

So the handler exists for a tick, or a handful of ticks, after an edit or a load, and at no other time. A balancer that nobody is editing has nothing registered and runs none of this mod's Lua, whatever is flowing through it.

4 days ago

it does not change the "no per-tick per-item Lua logic" claim.

That's not the claim that was being addressed at the start of this thread. That's the claim from the main mod page. I think I see now where the communication breakdown has occurred, though. If the claim is that the mod does not perform register an on_tick listener per-item being processed then yes, that claim is accurate. If the claim is that the mod does not register any on_tick listeners at all then no, that claim is misleading.

I think it's reasonable to go with the claim from the mod's front page, honestly, as that's the one most people will work off of. Apologies for the drawn-out thread to reach this point and thank you for taking the time to respond to my comments as well as your patience.

belt is laid against a balancer

This is not the behaviour I've been observing inside the game. I am seeing the on_tick event being fired when a belt is mined/placed up to 2 tiles away from a balancer. I don't think this is intended behaviour since you seem to be indicating it should only happen when a belt placed against a balancer is removed or placed. Would you like a bug report filed on GitHub for this or is this behaviour intentional?

Better Belt Balancer is written in Go and compiled to Lua by FkLua

100% personal opinion here but this is complete overkill. Your fk_module.lua file is a whopping 3 MB in size (uncompressed, naturally) which is kind of wild. All that code is loaded into the game engine and even if it's not executing it is still taking up resident memory and is being saved into the game state because of how Lua works under the hood. Perhaps it's the industry experiences I've had, working with embedded systems with very little spare memory, but having a huge volume of code that never gets used just rubs me the wrong way for whatever reason.

I'm glad it works for you and hats off for making a working mod. Don't let anything I've said or will say take away from the fact that you have made something that works and people are finding useful. Ultimately, that's what I think really matters.

Cheers.

4 days ago

The two-tile behavior is intentional; we have to work around how Factorio reports entity spawn/mine events.

The gate that decides whether a placed or mined belt-connectable concerns a balancer is a lookup in Better Belt Balancer's tile registry over the 5x5 neighborhood around the entity's position. It is two tiles rather than one because a splitter is two tiles wide and Factorio reports its position on the boundary between its halves, so a splitter whose far half touches a balancer reads as two tiles away from the tile that matters.

This leads to occasional false-positives, but they're easy enough to short-circuit in BBB's logic.


I agree that 3 MB of source actually loaded into RAM is large for what this mod does from a functionality perspective. I actually started work on an improved belt balancer mod 5 years ago but got so frustrated dealing with Lua that I shelved the project.

My new approach to Factorio modding has centered around using FkLua to let me mod the game using languages that don't make my eyes bleed. This is implemented as a WASM --> Lua compiler (or assembler or transpiler -- the language gets fuzzy when doing something like this).


For now, this conversion means paying substantial overhead in performance, file size, and memory usage over Lua-native mods.

This makes FkLua unsuitable for applications where lots of mod logic needs to execute on every tick -- for example, the original Belt Balancer mod (which did run per-item per-tick Lua code) would be a total non-starter if ported 1:1 to FkLua.

However, there are lots of mods (such as Better Belt Balancer) where Lua execution time is negligible, and for these applications, FkLua's overhead is immaterial.


The FkLua dream is for Wube to add a WASM interpreter as an alternate mod runtime to the current Lua interpreter natively in the Factorio engine. To achieve that, the FkLua project needs a lot of mods that are built on it to provide the incentive for Wube to support WASM natively.

While any mod that could be built on FkLua could be built in native Lua, the goal is to make the FkLua modding experience sufficiently superior to cause a natural shift in mod language choice. Not having to write Lua is a great carrot (IMO), but there are additional QoL features (both released and in development) to further entice modders to use FkLua/WASM.

If this dream comes true, FkLua mods will be portable to native WASM mods (hopefully with minimal modification), and all the awkward size/performance characteristics should fall away (in fact, performance should go up relative to native Lua mods because a native c++ WASM interpreter doesn't have to deal with Lua's eccentricities).


I hear you on the awkwardness of the oversized mod files; FkLua does what it can to prune unused interfaces to keep the archive size down, but ultimately it will never be competitive with native Lua without direct WASM interpretation in the engine. I did take a look at using LLVM IR as the IL for FkLua (like Emscripten did) to possibly reduce bloat, but ultimately determined that the LLVM ecosystem is fairly unfriendly to source-targeting backends (unlike WASM).

My professional career has mostly been in the cloud services space, but I have more than a couple embedded hobby projects -- I wouldn't dream of running this kind of crazy nested Go --> WASM --> Lua --> Interpreter chain on my ESP32s (though I would love it if TinyGo embedded were more mature so I don't have to keep writing bare C).

4 days ago

The two-tile behavior is intentional; we have to work around how Factorio reports entity spawn/mine events.

Alright, good to know. I won't file a GitHub issue then.

the language gets fuzzy when doing something like this

I do think "transpiler" is the correct term here, similar to was tsc does for TypeScript to JavaScript. "Compiler" almost always means a conversion into some non-human-readable format (e.g. machine code or some other intermediate language like Microsoft's CIL or Java's Java bytecode).

for Wube to add a WASM interpreter

I doubt that will happen, unfortunately, given they've announced 2.1 will be the final version save any major bug fixes or the like.

My professional career has mostly been in the cloud services space

Not a bad space to operate in, honestly. Lots of good work in that field! I come from an embedded/web background myself with a smattering of hardware accelerated graphics in the mix.

Feel free to poke my brain if you ever want to. My contacts are over on my GitHub (glektarssza).

3 days ago

I actually started work on an improved belt balancer mod 5 years ago but got so frustrated dealing with Lua that I shelved the project.

That's fair. Lua is easily the worst language I've ever had to use by a fair margin, especially when working with established code and unknown globals. The loose typing makes it incredibly hard to tell where things come from sometimes, especially stuff in "core".

3 days ago

I know right?

The last straw for me was an hours-long debugging session ending in the revelation that the Lua runtime was silently converting my array into a map at runtime because of the access pattern on the collection.

2 days ago

Arrays and maps are the same thing in Lua. They're all just maps/dictionaries. It just depends on how you index into them (a number versus a string-based key).

It's a big change from a more static typed language, 100% understand that feeling. Moving from something like C/C++ embedded side into the web world was a big leap for me too. I survived, though!

6 hours ago

It's not quite that clean cut. If it were it would just be painful, not horrifying.

A Lua table has an array and a hashtable internally. If you populate the table in an "array-like" way (i.e. using only dense integer keys), the records are stored in the array half of the table. Otherwise, the records are stored in the hashtable.

Except based on access/load/update pattern (esp setting values to nil) the runtime may at any point decide to "rehash" the table, which can move items between the array and hashtable.

Even that wouldn't be so bad if it weren't for the fact that some operations on tables (such as the length operator and table.insert/table.remove) have semantic differences depending on whether items are stored in the array half of the table or in the hashtable.

In practice, this means that the semantics of the data structure can change at runtime without warning and your only hope is to predict from your access pattern and knowledge of the runtime's rules what will and will not change the underlying collection type during a rehash to preserve which sub-collection your items are in inside the table.

New response