class_name PropUseSpotData extends RefCounted ## Pure validation and conversion helpers for placed-prop Use spots. static func validate_saved(raw_spots: Variant, raw_next: Variant) -> String: if not raw_spots is Array: return "Prop Use spots must be an array." if not _is_integer_number(raw_next): return "Prop Use spot counter must be an integer." var next_value: float = float(raw_next) if not is_finite(next_value) or next_value < 1.0: return "Prop Use spot counter must be a positive integer." var ids: Dictionary = {} var numbers: Dictionary = {} var highest: int = 0 for raw: Variant in raw_spots as Array: if not raw is Dictionary: return "Prop Use spot entries must be objects." var spot: Dictionary = raw var raw_id: Variant = spot.get("id", null) var raw_number: Variant = spot.get("number", null) var raw_name: Variant = spot.get("name", null) if not raw_id is String or not _is_integer_number(raw_number) or not raw_name is String: return "Prop Use spot fields are invalid." var numeric_number: float = float(raw_number) if not is_finite(numeric_number) or numeric_number < 1.0: return "Prop Use spot numbers must be positive integers." var number: int = int(numeric_number) var spot_id: String = String(raw_id) if spot_id != "spot_%d" % number or ids.has(spot_id) or numbers.has(number): return "Prop Use spot IDs and numbers must be unique and canonical." if not _is_finite_point(spot.get("position", null)): return "Prop Use spot positions must be finite points." ids[spot_id] = true numbers[number] = true highest = maxi(highest, number) if int(next_value) <= highest: return "Prop Use spot counter must exceed every existing spot number." return "" static func decode(raw_spots: Array) -> Array[Dictionary]: var result: Array[Dictionary] = [] for raw: Variant in raw_spots: if not raw is Dictionary: continue var spot: Dictionary = raw var number: int = int(spot.get("number", 0)) var name: String = String(spot.get("name", "")).strip_edges() result.append({ "id": String(spot.get("id", "spot_%d" % number)), "number": number, "name": name if not name.is_empty() else fallback_name(number), "position": _point_from_data(spot.get("position", null)), }) return result static func encode(spots: Array[Dictionary]) -> Array[Dictionary]: var result: Array[Dictionary] = [] for spot: Dictionary in spots: var number: int = int(spot.get("number", 0)) var position: Variant = spot.get("position", Vector2.ZERO) if not position is Vector2: continue var name: String = String(spot.get("name", "")).strip_edges() result.append({ "id": String(spot.get("id", "spot_%d" % number)), "number": number, "name": name if not name.is_empty() else fallback_name(number), "position": {"x": position.x, "y": position.y}, }) return result static func fallback_name(number: int) -> String: return "Spot %d" % maxi(1, number) static func find(spots: Array[Dictionary], spot_id: String) -> Dictionary: for spot: Dictionary in spots: if String(spot.get("id", "")) == spot_id: return spot.duplicate(true) return {} static func _is_finite_point(value: Variant) -> bool: if not value is Dictionary: return false var point: Dictionary = value var x: Variant = point.get("x", null) var y: Variant = point.get("y", null) return (x is int or x is float) and (y is int or y is float) and is_finite(float(x)) and is_finite(float(y)) static func _is_integer_number(value: Variant) -> bool: if value is int: return true if not value is float: return false var number: float = value return is_finite(number) and absf(number) <= 9007199254740991.0 and floorf(number) == number static func _point_from_data(value: Variant) -> Vector2: if not _is_finite_point(value): return Vector2.ZERO var point: Dictionary = value return Vector2(float(point["x"]), float(point["y"]))