tfmgfix-2.1.0
Curse Maven Snippet
What's new
2.1.0 — for TFMG 1.2.2
Same target as 2.0.0: TFMG 1.2.2 only. Stay on tfmgfix-1.2.0.jar if you are still on TFMG
1.2.0. Everything 2.0.0 fixed is still fixed; this adds one thing and changes nothing else.
Fixed: building a network is quadratic, and it happens inside a tick
Walk into a large electrical build and the game stops for a moment. Measured on a live server with
an instrumented client: one client tick of 2381 ms, and further ones of 336, 102, 73, 72, 61 and
45 ms, every one of them inside Level.tickBlockEntities. Sampled stacks named the same path in
five stalls out of six.
ElectricalNetwork.add is the only way a block joins a network, and it is written like this:
public void add(IElectric e) {
List<Long> ids = new ArrayList<>(); // a fresh list, every call
members.forEach(m -> ids.add(m.getData().getId())); // every member, every id boxed
if (ids.contains(e.getData().getId())) return; // linear scan
members.add(e);
}
One membership test walks the whole network and allocates one boxed Long per member. The caller is
IElectric.onConnected, which recurses across the entire grid, and that is reached from onPlaced,
which tickElectricity runs whenever connectNextTick is set — and readElectricity sets it, so
every electrical block asks for a full rebuild as its chunk loads. Rebuilding a grid of n blocks
therefore costs O(n²) time and allocates on the order of n² objects: for a two-thousand-block grid,
four million of them, in one tick, on the game thread.
The replacement compares the same field, in the same order, against the same live members, and returns the same answer for every possible input — without the list, without the boxing, and stopping at the first match.
Measured after the change, same instrument, same kind of walk: worst tick 2381 → 570 ms, and the tail 336 → 89, 103 → 64, 73 → 47. About a quarter of what it was.
Why it is not an O(1) lookup, which is what it obviously wants to be
Because the thing being compared is not an identity. ElectricBlockValues.getId() returns the field
electricalNetworkId — the id of the network the block currently believes it belongs to — and that
field is written at runtime by IElectric.setNetwork(long) and IElectric.onPlaced(), both of
which run inside the very recursion that calls add. Scanning the constant pools of all 678 classes
in the jar confirms those two, plus the constructor, are every write there is.
So a cached set of member ids goes stale silently, and the size of members does not change when it
does. Concretely: member A joins with id 5; something calls setNetwork(9) on A; add(B) arrives
with B also on 9. The original rebuilds the id list from the live members, sees 9, and refuses B. A
cache holding {5} sees no 9 and adds B to a network the original would have left it out of — a
different grid, a different voltage, a different answer to who has power.
That is a mechanics change wearing the costume of an optimisation, so it is not in this build. The
remaining cost is still O(n) per join by design. Making the membership test independent of a mutable
field decides which blocks land in which network, and a third-party patch may not decide that — it
belongs upstream, together with readElectricity scheduling a full rebuild per block on chunk load.
Unchanged
Everything from 2.0.0: the deferred-action queue that was never emptied, the quadratic network update
(getCableCurrent computed once per update instead of once per cable), the large switch reconnect
throttle, and the three goggle tooltips that crash the client when a multiblock loses its controller.
The cached cable current was re-verified against the same trap this release documents: nothing
reachable from the third pass of updateNetwork writes electricalNetworkId or calls setNetwork /
onPlaced, so no member can move to another network mid-pass and the cached value stays correct.
Verified before release
membersis a plainArrayList, so the indexed loop is O(1) per step and not accidentally quadratic on a linked list.- Every
getData()in the jar is a three-instruction getter (aload_0; getfield; areturn) — four implementations, all pure. So evaluating the incoming id before the walk instead of after it cannot change anything, and nothing can modify the list while the walk is in progress. Long.equalsand==agree on everylong.- Injection points checked against
tfmg-1.2.2.jarbefore compiling; the jar carries no local paths and credits its author.
This mod has no additional files

