Capture current source, content data, tests, asset licenses, and original-specification traceability. Original implementation remains incomplete. The accepted matching UI generation passed 154 suites and 12,253 checks, with eight rendered staff-contract screens reviewed. Latest native Windows and full-original acceptance remain outstanding.
209 lines
7.3 KiB
GDScript
209 lines
7.3 KiB
GDScript
class_name FacilityUpkeepAccrual
|
|
extends RefCounted
|
|
|
|
## Exact upkeep accumulation in gold-minute units. The caller supplies the
|
|
## applicable daily rates for [current_minute, target_minute); this component
|
|
## deliberately does not decide whether closed/suspended rooms are billable.
|
|
## No money is rounded, debited, or forgiven here. Settlement owns those rules.
|
|
const SNAPSHOT_VERSION := 1
|
|
const MINUTES_PER_DAY := 1440
|
|
const MAX_DAILY_RATE := 1000000000
|
|
const MAX_FACILITIES := 20000
|
|
const MAX_PERIODS := 10000
|
|
const MAX_MINUTE := 1000000000
|
|
|
|
var _started_at := -1
|
|
var _current_minute := -1
|
|
var _period_start := -1
|
|
var _pending_units: Dictionary = {}
|
|
var _closed_periods: Array[Dictionary] = []
|
|
|
|
|
|
func start_at(minute: int) -> Dictionary:
|
|
if _started_at >= 0:
|
|
return _failure(&"ALREADY_STARTED")
|
|
if minute < 0 or minute > MAX_MINUTE:
|
|
return _failure(&"INVALID_MINUTE")
|
|
_started_at = minute
|
|
_current_minute = minute
|
|
_period_start = minute
|
|
return {&"ok": true}
|
|
|
|
|
|
func get_current_minute() -> int:
|
|
return _current_minute
|
|
|
|
|
|
func advance_to(minute: int, daily_rates: Dictionary) -> Dictionary:
|
|
if _started_at < 0:
|
|
return _failure(&"NOT_STARTED")
|
|
if minute < _current_minute or minute > MAX_MINUTE:
|
|
return _failure(&"INVALID_MINUTE")
|
|
# A bounded batch also supports migration/testing without an unbounded loop.
|
|
if minute - _current_minute > MINUTES_PER_DAY:
|
|
return _failure(&"INTERVAL_TOO_LARGE")
|
|
var parsed := _parse_units(daily_rates, MAX_DAILY_RATE)
|
|
if not parsed[&"ok"]:
|
|
return _failure(&"INVALID_RATES")
|
|
var rates: Dictionary = parsed[&"value"]
|
|
var touched := _pending_units.duplicate()
|
|
for id: String in rates:
|
|
if int(rates[id]) > 0:
|
|
touched[id] = 0
|
|
if touched.size() > MAX_FACILITIES:
|
|
return _failure(&"TOO_MANY_FACILITIES")
|
|
var closures := minute / MINUTES_PER_DAY - _current_minute / MINUTES_PER_DAY
|
|
if _closed_periods.size() + closures > MAX_PERIODS:
|
|
return _failure(&"TOO_MANY_PERIODS")
|
|
if minute == _current_minute:
|
|
return {&"ok": true, &"advanced_minutes": 0, &"closed_periods": []}
|
|
var before := _current_minute
|
|
var newly_closed: Array[Dictionary] = []
|
|
while _current_minute < minute:
|
|
var midnight := (_current_minute / MINUTES_PER_DAY + 1) * MINUTES_PER_DAY
|
|
var until := mini(midnight, minute)
|
|
var elapsed := until - _current_minute
|
|
for id: String in rates:
|
|
var rate := int(rates[id])
|
|
if rate > 0:
|
|
_pending_units[id] = int(_pending_units.get(id, 0)) + rate * elapsed
|
|
_current_minute = until
|
|
if until == midnight:
|
|
var closed := {
|
|
&"period_start": _period_start,
|
|
&"period_end": until,
|
|
&"facility_units": _canonical_units(_pending_units),
|
|
}
|
|
_closed_periods.append(closed)
|
|
newly_closed.append(closed.duplicate(true))
|
|
_pending_units = {}
|
|
_period_start = until
|
|
return {&"ok": true, &"advanced_minutes": minute - before, &"closed_periods": newly_closed}
|
|
|
|
|
|
func get_unsettled_units(facility_id: StringName, settled_periods: int) -> int:
|
|
var id := String(facility_id)
|
|
var units := int(_pending_units.get(id, 0))
|
|
for index in range(clampi(settled_periods, 0, _closed_periods.size()), _closed_periods.size()):
|
|
units += int(_closed_periods[index][&"facility_units"].get(id, 0))
|
|
return units
|
|
|
|
|
|
func get_closed_periods_from(index: int) -> Array:
|
|
return _closed_periods.slice(clampi(index, 0, _closed_periods.size())).duplicate(true)
|
|
|
|
|
|
func get_report() -> Dictionary:
|
|
return {
|
|
&"current_minute": _current_minute,
|
|
&"period_start": _period_start,
|
|
&"gold_denominator": MINUTES_PER_DAY,
|
|
&"pending_facility_units": _canonical_units(_pending_units),
|
|
&"closed_periods": _closed_periods.duplicate(true),
|
|
}
|
|
|
|
|
|
func create_snapshot() -> Dictionary:
|
|
return {
|
|
&"version": SNAPSHOT_VERSION,
|
|
&"started_at": _started_at,
|
|
&"current_minute": _current_minute,
|
|
&"period_start": _period_start,
|
|
&"pending_units": _canonical_units(_pending_units),
|
|
&"closed_periods": _closed_periods.duplicate(true),
|
|
}
|
|
|
|
|
|
func restore_snapshot(source: Dictionary) -> bool:
|
|
# Parse and cross-check the complete candidate before mutating live state.
|
|
if not _exact_keys(source, ["version", "started_at", "current_minute", "period_start", "pending_units", "closed_periods"]):
|
|
return false
|
|
for key: String in ["version", "started_at", "current_minute", "period_start"]:
|
|
if typeof(source[key]) != TYPE_INT:
|
|
return false
|
|
if source["version"] != SNAPSHOT_VERSION or not source["closed_periods"] is Array:
|
|
return false
|
|
var started := int(source["started_at"])
|
|
var current := int(source["current_minute"])
|
|
var period_start := int(source["period_start"])
|
|
var raw_periods: Array = source["closed_periods"]
|
|
if raw_periods.size() > MAX_PERIODS:
|
|
return false
|
|
if started == -1:
|
|
if current != -1 or period_start != -1 or not raw_periods.is_empty() \
|
|
or not source["pending_units"] is Dictionary or not source["pending_units"].is_empty():
|
|
return false
|
|
elif started < 0 or current < started or current > MAX_MINUTE \
|
|
or period_start < started or period_start > current \
|
|
or period_start / MINUTES_PER_DAY != current / MINUTES_PER_DAY:
|
|
return false
|
|
var periods: Array[Dictionary] = []
|
|
var expected_start := started
|
|
for raw: Variant in raw_periods:
|
|
if not raw is Dictionary or not _exact_keys(raw, ["period_start", "period_end", "facility_units"]):
|
|
return false
|
|
if typeof(raw["period_start"]) != TYPE_INT or typeof(raw["period_end"]) != TYPE_INT:
|
|
return false
|
|
var end := int(raw["period_end"])
|
|
if raw["period_start"] != expected_start or end > current \
|
|
or end != (expected_start / MINUTES_PER_DAY + 1) * MINUTES_PER_DAY:
|
|
return false
|
|
var units := _parse_units(raw["facility_units"], MAX_DAILY_RATE * (end - expected_start), false)
|
|
if not units[&"ok"]:
|
|
return false
|
|
periods.append({&"period_start": expected_start, &"period_end": end, &"facility_units": units[&"value"]})
|
|
expected_start = end
|
|
if expected_start != period_start:
|
|
return false
|
|
var pending := _parse_units(source["pending_units"], MAX_DAILY_RATE * maxi(current - period_start, 0), false)
|
|
if not pending[&"ok"]:
|
|
return false
|
|
_started_at = started
|
|
_current_minute = current
|
|
_period_start = period_start
|
|
_pending_units = pending[&"value"]
|
|
_closed_periods = periods
|
|
return true
|
|
|
|
|
|
static func _parse_units(value: Variant, maximum: int, allow_zero := true) -> Dictionary:
|
|
if not value is Dictionary or value.size() > MAX_FACILITIES:
|
|
return {&"ok": false}
|
|
var result: Dictionary = {}
|
|
for raw_id: Variant in value:
|
|
if typeof(raw_id) not in [TYPE_STRING, TYPE_STRING_NAME]:
|
|
return {&"ok": false}
|
|
var id := String(raw_id)
|
|
if id.is_empty() or id.length() > 128 or result.has(id):
|
|
return {&"ok": false}
|
|
var amount: Variant = value[raw_id]
|
|
if typeof(amount) != TYPE_INT or amount < (0 if allow_zero else 1) or amount > maximum:
|
|
return {&"ok": false}
|
|
result[id] = amount
|
|
return {&"ok": true, &"value": _canonical_units(result)}
|
|
|
|
|
|
static func _canonical_units(source: Dictionary) -> Dictionary:
|
|
var keys := source.keys()
|
|
keys.sort()
|
|
var result: Dictionary = {}
|
|
for key: Variant in keys:
|
|
result[String(key)] = source[key]
|
|
return result
|
|
|
|
|
|
static func _exact_keys(value: Dictionary, expected: Array) -> bool:
|
|
if value.size() != expected.size():
|
|
return false
|
|
for key: Variant in value:
|
|
if typeof(key) not in [TYPE_STRING, TYPE_STRING_NAME] or String(key) not in expected:
|
|
return false
|
|
for key: String in expected:
|
|
if not value.has(key):
|
|
return false
|
|
return true
|
|
|
|
|
|
static func _failure(reason: StringName) -> Dictionary:
|
|
return {&"ok": false, &"reason": reason}
|