OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / game / terrain_edit_order.gd

62 lines · gd ref:
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])]