class_name PresentationRelevancePolicy extends RefCounted const DEFAULT_DETAIL_LIMIT := 40 const MAX_DETAIL_LIMIT := 40 const IMPORTANCE_SCORE_SCALE := 1_000_000 const NAMED_SCORE_BONUS := 125_000 const MAX_DISTANCE_PENALTY := 1_000_000 const PIN_FIELDS := { "pinned": "pin_explicit", "player_affecting_threat": "pin_player_affecting_threat", "situation_participant": "pin_situation_participant", "commitment_participant": "pin_commitment_participant", "leader": "pin_leader", "unique_owner": "pin_unique_owner", } const ALLOWED_DESCRIPTOR_FIELDS := { "stable_id": true, "context_id": true, "address": true, "importance": true, "named": true, "loaded": true, "pinned": true, "player_affecting_threat": true, "situation_participant": true, "commitment_participant": true, "leader": true, "unique_owner": true, } func select_detailed_visuals( active_context: Dictionary, player_address: SpatialAddress, candidate_descriptors: Array, detail_limit: int = DEFAULT_DETAIL_LIMIT ) -> Dictionary: var errors: Array[String] = [] var normalized_context := _normalize_context(active_context, errors) _validate_player_address(player_address, normalized_context, errors) if detail_limit < 1 or detail_limit > MAX_DETAIL_LIMIT: errors.append("detail_limit must be between 1 and %d" % MAX_DETAIL_LIMIT) if not errors.is_empty(): return _invalid_result(detail_limit, errors) var ranked: Array[Dictionary] = [] var rejected: Array[Dictionary] = [] var seen_ids: Dictionary = {} for candidate_index in candidate_descriptors.size(): var raw_candidate: Variant = candidate_descriptors[candidate_index] var candidate_errors: Array[String] = [] var normalized := _normalize_candidate(raw_candidate, candidate_index, candidate_errors) if not candidate_errors.is_empty(): errors.append_array(candidate_errors) continue var stable_id: StringName = normalized["stable_id"] if seen_ids.has(stable_id): errors.append("duplicate candidate stable_id '%s'" % stable_id) continue seen_ids[stable_id] = true var scope_rejection := _scope_rejection(normalized, normalized_context) if not scope_rejection.is_empty(): rejected.append(_rejected_scope_decision(normalized, scope_rejection)) continue ranked.append(_rank_candidate(normalized, player_address)) if not errors.is_empty(): return _invalid_result(detail_limit, errors) ranked.sort_custom(_rank_before) var mandatory_count := 0 for entry in ranked: if bool(entry["mandatory"]): mandatory_count += 1 if mandatory_count > detail_limit: return _pin_overflow_result(detail_limit, mandatory_count, ranked, rejected) var selected_ids: Array[StringName] = [] var selected_details: Array[Dictionary] = [] for rank_index in ranked.size(): var entry: Dictionary = ranked[rank_index] var decision := _decision_from_ranked_entry(entry) if rank_index < detail_limit: decision["selected"] = true decision["reason_trace"].append( ( "selected_mandatory_pin" if bool(entry["mandatory"]) else "selected_rank_%d" % rank_index ) ) selected_ids.append(entry["stable_id"]) selected_details.append(decision) else: decision["reason_trace"].append("rejected_detail_capacity_rank_%d" % rank_index) rejected.append(decision) rejected.sort_custom(_stable_id_before) var decisions: Array[Dictionary] = selected_details.duplicate(true) decisions.append_array(rejected) decisions.sort_custom(_stable_id_before) return { "valid": true, "detail_limit": detail_limit, "candidate_count": candidate_descriptors.size(), "eligible_count": ranked.size(), "mandatory_count": mandatory_count, "selected_ids": selected_ids, "selected_details": selected_details, "rejected_ids": _decision_ids(rejected), "decisions": decisions, "errors": [] as Array[String], } func get_decision(result: Dictionary, stable_id: StringName) -> Dictionary: for value in result.get("decisions", []): var decision := value as Dictionary if StringName(decision.get("stable_id", &"")) == stable_id: return decision.duplicate(true) return {} func _normalize_context(active_context: Dictionary, errors: Array[String]) -> Dictionary: var result := {} for field in ["context_id", "world_id", "location_id"]: var value: Variant = active_context.get(field) if (value is not String and value is not StringName) or String(value).is_empty(): errors.append("active_context.%s must be a non-empty stable ID" % field) continue result[field] = StringName(value) return result func _validate_player_address( player_address: SpatialAddress, active_context: Dictionary, errors: Array[String] ) -> void: if player_address == null or not player_address.is_valid(): errors.append("player_address must be a valid SpatialAddress") return if active_context.is_empty(): return if player_address.get_world_id() != active_context.get("world_id", &""): errors.append("player_address belongs to another world") if player_address.get_location_id() != active_context.get("location_id", &""): errors.append("player_address belongs to another location") func _normalize_candidate( raw_candidate: Variant, candidate_index: int, errors: Array[String] ) -> Dictionary: if raw_candidate is not Dictionary: errors.append("candidate %d must be a Dictionary descriptor" % candidate_index) return {} var descriptor: Dictionary = raw_candidate for field in descriptor: if not ALLOWED_DESCRIPTOR_FIELDS.has(String(field)): errors.append("candidate %d has unsupported field '%s'" % [candidate_index, field]) var stable_id_value: Variant = descriptor.get("stable_id") var context_id_value: Variant = descriptor.get("context_id") var stable_id := _normalize_descriptor_id(stable_id_value, candidate_index, "stable_id", errors) var context_id := _normalize_descriptor_id( context_id_value, candidate_index, "context_id", errors ) var address := descriptor.get("address") as SpatialAddress if address == null or not address.is_valid(): errors.append("candidate %d address must be a valid SpatialAddress" % candidate_index) elif address.get_world_id().is_empty() or address.get_location_id().is_empty(): errors.append("candidate %d address must name a world and location" % candidate_index) var importance_value: Variant = descriptor.get("importance") var importance := 0.0 if importance_value is not int and importance_value is not float: errors.append("candidate %d importance must be numeric" % candidate_index) else: importance = float(importance_value) if not is_finite(importance) or importance < 0.0 or importance > 1.0: errors.append( "candidate %d importance must be finite and within [0, 1]" % candidate_index ) var normalized := { "stable_id": stable_id, "context_id": context_id, "address": address.duplicate_address() if address != null else null, "importance": importance, "named": _normalize_bool(descriptor, "named", candidate_index, errors, false), "loaded": _normalize_bool(descriptor, "loaded", candidate_index, errors, false), } for pin_field in PIN_FIELDS: normalized[pin_field] = _normalize_bool( descriptor, String(pin_field), candidate_index, errors, false ) return normalized func _normalize_descriptor_id( value: Variant, candidate_index: int, field: String, errors: Array[String] ) -> StringName: if (value is not String and value is not StringName) or String(value).is_empty(): errors.append("candidate %d %s must be a non-empty stable ID" % [candidate_index, field]) return &"" return StringName(value) func _normalize_bool( descriptor: Dictionary, field: String, candidate_index: int, errors: Array[String], default_value: bool ) -> bool: if not descriptor.has(field): return default_value var value: Variant = descriptor[field] if value is not bool: errors.append("candidate %d %s must be bool" % [candidate_index, field]) return default_value return bool(value) func _scope_rejection(candidate: Dictionary, active_context: Dictionary) -> String: if candidate["context_id"] != active_context["context_id"]: return "rejected_inactive_context" var address: SpatialAddress = candidate["address"] if address.get_world_id() != active_context["world_id"]: return "rejected_different_world" if address.get_location_id() != active_context["location_id"]: return "rejected_different_location" return "" func _rejected_scope_decision(candidate: Dictionary, reason: String) -> Dictionary: var pin_reasons := _pin_reasons(candidate) var trace: Array[String] = [reason] if not pin_reasons.is_empty(): trace.append("pin_ignored_outside_active_scope") trace.append_array(pin_reasons) return { "stable_id": candidate["stable_id"], "selected": false, "mandatory": not pin_reasons.is_empty(), "score_units": 0, "distance_millimeters": -1, "reason_trace": trace, } func _rank_candidate(candidate: Dictionary, player_address: SpatialAddress) -> Dictionary: var address: SpatialAddress = candidate["address"] var distance_millimeters := roundi( player_address.get_position().distance_to(address.get_position()) * 1000.0 ) var importance_units := roundi(float(candidate["importance"]) * IMPORTANCE_SCORE_SCALE) var named_bonus := NAMED_SCORE_BONUS if bool(candidate["named"]) else 0 var distance_penalty := mini(distance_millimeters, MAX_DISTANCE_PENALTY) var pin_reasons := _pin_reasons(candidate) var trace: Array[String] = [ "eligible_active_context", "importance_units_%d" % importance_units, "distance_millimeters_%d" % distance_millimeters, ] if bool(candidate["named"]): trace.append("named_entity_bonus_%d" % NAMED_SCORE_BONUS) if bool(candidate["loaded"]): trace.append("load_state_ignored") trace.append_array(pin_reasons) return { "stable_id": candidate["stable_id"], "mandatory": not pin_reasons.is_empty(), "score_units": importance_units + named_bonus - distance_penalty, "distance_millimeters": distance_millimeters, "reason_trace": trace, } func _pin_reasons(candidate: Dictionary) -> Array[String]: var result: Array[String] = [] for field in PIN_FIELDS: if bool(candidate.get(field, false)): result.append(String(PIN_FIELDS[field])) return result func _decision_from_ranked_entry(entry: Dictionary) -> Dictionary: return { "stable_id": entry["stable_id"], "selected": false, "mandatory": entry["mandatory"], "score_units": entry["score_units"], "distance_millimeters": entry["distance_millimeters"], "reason_trace": (entry["reason_trace"] as Array).duplicate(), } func _pin_overflow_result( detail_limit: int, mandatory_count: int, ranked: Array[Dictionary], rejected: Array[Dictionary] ) -> Dictionary: var decisions: Array[Dictionary] = rejected.duplicate(true) for entry in ranked: var decision := _decision_from_ranked_entry(entry) decision["reason_trace"].append("rejected_unsatisfiable_pin_overflow") decisions.append(decision) decisions.sort_custom(_stable_id_before) return { "valid": false, "detail_limit": detail_limit, "candidate_count": ranked.size() + rejected.size(), "eligible_count": ranked.size(), "mandatory_count": mandatory_count, "selected_ids": [] as Array[StringName], "selected_details": [] as Array[Dictionary], "rejected_ids": _decision_ids(decisions), "decisions": decisions, "errors": [ ( "mandatory_pin_overflow: %d mandatory candidates exceed detail limit %d" % [mandatory_count, detail_limit] ) ], } func _invalid_result(detail_limit: int, errors: Array[String]) -> Dictionary: errors.sort() return { "valid": false, "detail_limit": detail_limit, "candidate_count": 0, "eligible_count": 0, "mandatory_count": 0, "selected_ids": [] as Array[StringName], "selected_details": [] as Array[Dictionary], "rejected_ids": [] as Array[StringName], "decisions": [] as Array[Dictionary], "errors": errors.duplicate(), } func _decision_ids(decisions: Array[Dictionary]) -> Array[StringName]: var result: Array[StringName] = [] for decision in decisions: result.append(StringName(decision["stable_id"])) return result func _rank_before(first: Dictionary, second: Dictionary) -> bool: if bool(first["mandatory"]) != bool(second["mandatory"]): return bool(first["mandatory"]) if int(first["score_units"]) != int(second["score_units"]): return int(first["score_units"]) > int(second["score_units"]) if int(first["distance_millimeters"]) != int(second["distance_millimeters"]): return int(first["distance_millimeters"]) < int(second["distance_millimeters"]) return String(first["stable_id"]) < String(second["stable_id"]) func _stable_id_before(first: Dictionary, second: Dictionary) -> bool: return String(first["stable_id"]) < String(second["stable_id"])