extends RefCounted
class_name TerrainEditOrder
## THE TOTAL ORDER OVER TERRAIN EDIT RECORDS — hyper-games #42 P1.
##
## Its own file, with NO autoload references, for two reasons.
##
## 1. It is pure logic over key strings and nothing about it needs a room, a peer
## or a terrain. The level journal's fold lives in libdc for the same reason —
## "shared with any other consumer rather than living in whichever UI happened
## to need it first".
## 2. A `--script` gate is COMPILED BEFORE THE AUTOLOADS ARE REGISTERED
## (state_routes_gate.gd:47), so preloading anything that names `GameState` is
## a compile error — and the failure presents as the gate HANGING to its
## timeout, not as a parse error you can read. Leaving the comparator inside
## TerrainEditManager made it untestable by exactly the gate that needs it.
##
## WHY AN ORDER AT ALL. DCEditList composes with min (subtract) and max (add), and
## those do not commute: base 0, add 5, subtract -3 gives -3 one way and +5 the
## other; two overlapping hard spheres come out -1.5 against +1.5, opposite SIGNS
## (dc-terrain libdc/test/test_edit_order.cpp). Replay was unsorted, so two peers
## holding the same journal could materialise different geometry.
const PREFIX := "world:terrain:edit:"
## Sorted by (seq, author) — the comparator the level journal already folds by.
##
## SEQ FIRST, AUTHOR ONLY TO BREAK TIES. Concurrent edits have no true order; the
## requirement is a stable arbitrary one every peer computes identically, not a
## correct one, which does not exist.
##
## DECORATE-SORT-UNDECORATE, with no Callable. sort_custom with an inline
## multi-line lambda hangs a headless run rather than failing to parse
## (feedback_gdscript-lambda-hangs-headless), and a zero-padded seq followed by the
## author sorts identically under plain string comparison — so the built-in sort()
## does the work and there is nothing to hang. 20 digits covers the wall-clock
## value reserve_seq seeds from.
static func sort_keys(keys: Array[String]) -> Array[String]:
var decorated: Array[String] = []
for k: String in keys:
var dot: Array = dot_of(k)
decorated.append("%020d|%s|%s" % [int(dot[1]), String(dot[0]), k])
decorated.sort()
var out: Array[String] = []
for d: String in decorated:
out.append(d.substr(d.find("|", d.find("|") + 1) + 1))
return out
## "world:terrain:edit:<author>:<seq>" -> [author, seq].
##
## rsplit on the LAST colon, because a memberId may contain colons and the seq
## never does. A malformed key returns seq 0 rather than being dropped: a record we
## cannot place is still a record, and discarding it silently is a worse failure
## than ordering it oddly.
static func dot_of(key: String) -> Array:
var eid: String = key.replace(PREFIX, "")
var parts: PackedStringArray = eid.rsplit(":", true, 1)
if parts.size() != 2:
return [eid, 0]
return [parts[0], int(parts[1])]