P18-01-CODE: 原本関係モデル・旧値保全・Runtime保存と読取経路
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
class_name OriginalRelationshipState
|
||||
extends RefCounted
|
||||
|
||||
## Explicit facts only. Numeric values are not production initialization approval.
|
||||
## No update, attack, reduction, threshold or route policy is defined here.
|
||||
const FIELDS := ["subject_id", "protagonist_id", "rival_id", "affection", "lust", "netorare", "shield_current", "shield_max"]
|
||||
var _payload: Dictionary = {}
|
||||
|
||||
|
||||
static func from_snapshot(value: Variant) -> OriginalRelationshipState:
|
||||
if not value is Dictionary or value.size() != FIELDS.size():
|
||||
return null
|
||||
for field: String in FIELDS:
|
||||
if not value.has(field):
|
||||
return null
|
||||
for field: String in ["subject_id", "protagonist_id", "rival_id"]:
|
||||
if not (value[field] is String or value[field] is StringName) or String(value[field]).is_empty():
|
||||
return null
|
||||
if value["subject_id"] == value["protagonist_id"] or value["subject_id"] == value["rival_id"] or value["protagonist_id"] == value["rival_id"]:
|
||||
return null
|
||||
for field: String in ["affection", "lust", "netorare", "shield_current", "shield_max"]:
|
||||
if not (value[field] is int or value[field] is float):
|
||||
return null
|
||||
if value[field] is float and not is_finite(value[field]):
|
||||
return null
|
||||
if value["shield_max"] != value["affection"] or value["shield_current"] < 0 or value["shield_current"] > value["shield_max"]:
|
||||
return null
|
||||
var state := OriginalRelationshipState.new()
|
||||
state._payload = value.duplicate(true)
|
||||
return state
|
||||
|
||||
|
||||
func create_snapshot() -> Dictionary:
|
||||
return _payload.duplicate(true)
|
||||
|
||||
|
||||
func subject_id() -> StringName:
|
||||
return StringName(_payload.get("subject_id", ""))
|
||||
@@ -0,0 +1 @@
|
||||
uid://cqxwpbkgjtnsa
|
||||
@@ -9,8 +9,9 @@ signal route_lock_changed(relationship_id: StringName, old_lock: int, new_lock:
|
||||
|
||||
const RelationshipStateType = preload("res://scripts/relationship/relationship_state.gd")
|
||||
const CommandResultType = preload("res://scripts/core/command_result.gd")
|
||||
const OriginalStateType = preload("res://scripts/relationship/original_relationship_state.gd")
|
||||
|
||||
const SNAPSHOT_VERSION := 1
|
||||
const SNAPSHOT_VERSION := 2
|
||||
const DAILY_AUTOMATIC_DELTA_LIMIT := 10
|
||||
const GATED_THRESHOLDS: Array[int] = [50, 80]
|
||||
const STAGE_THRESHOLDS: Array[int] = [20, 50, 80, 100]
|
||||
@@ -22,6 +23,46 @@ var _pair_index: Dictionary = {}
|
||||
var _threshold_candidates: Array[Dictionary] = []
|
||||
var _next_candidate_sequence := 1
|
||||
var _candidate_callback := Callable()
|
||||
var _original_records: Dictionary = {}
|
||||
var _original_identity_validator := Callable()
|
||||
var _legacy_raw: Dictionary = {}
|
||||
var _legacy_baselines: Dictionary = {}
|
||||
var _container_raw: Dictionary = {}
|
||||
|
||||
|
||||
func configure_original_identity_validator(callback: Callable) -> RelationshipService:
|
||||
_original_identity_validator = callback
|
||||
return self
|
||||
|
||||
|
||||
func register_original_relationship(payload: Variant) -> CommandResult:
|
||||
var state := OriginalStateType.from_snapshot(payload)
|
||||
if state == null:
|
||||
return _invalid("Original relationship requires complete explicit facts.")
|
||||
if not _original_identity_validator.is_valid() or not bool(_original_identity_validator.call(state.create_snapshot())):
|
||||
return _invalid("Original relationship identities are not registered.")
|
||||
if _original_records.has(state.subject_id()):
|
||||
return _duplicate("Original relationship subject", state.subject_id())
|
||||
_original_records[state.subject_id()] = state.create_snapshot()
|
||||
last_error = ""
|
||||
return _success({&"subject_id": state.subject_id()})
|
||||
|
||||
|
||||
func get_original_relationship(subject_id: StringName) -> Dictionary:
|
||||
if _original_records.has(subject_id):
|
||||
var result: Dictionary = _original_records[subject_id].duplicate(true)
|
||||
result[&"configured"] = true
|
||||
return result
|
||||
return {&"configured": false, &"subject_id": subject_id,
|
||||
&"protagonist_id": null, &"rival_id": null, &"affection": null,
|
||||
&"lust": null, &"netorare": null, &"shield_current": null, &"shield_max": null}
|
||||
|
||||
|
||||
func validate_original_identities() -> bool:
|
||||
for payload: Dictionary in _original_records.values():
|
||||
if not _original_identity_validator.is_valid() or not bool(_original_identity_validator.call(payload.duplicate(true))):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func configure_candidate_callback(callback: Callable = Callable()) -> RelationshipService:
|
||||
@@ -329,6 +370,8 @@ func consume_threshold_candidate(candidate_id: StringName) -> Dictionary:
|
||||
|
||||
func validate_invariants() -> Dictionary:
|
||||
var errors: Array[String] = []
|
||||
if not validate_original_identities():
|
||||
errors.append("Original relationship identity reference is invalid.")
|
||||
var seen_pairs: Dictionary = {}
|
||||
for relationship_id: StringName in _sorted_name_keys(_relationships):
|
||||
var state := get_relationship(relationship_id)
|
||||
@@ -347,22 +390,92 @@ func validate_invariants() -> Dictionary:
|
||||
func create_snapshot() -> Dictionary:
|
||||
var relationships: Array[Dictionary] = []
|
||||
for state: RelationshipState in get_relationships():
|
||||
relationships.append(state.create_snapshot())
|
||||
var current := state.create_snapshot()
|
||||
var original: Dictionary = _legacy_baselines.get(state.relationship_id, {})
|
||||
var raw: Dictionary = {}
|
||||
for entry: Dictionary in _legacy_raw.get(&"relationships", []):
|
||||
if StringName(_value(entry, &"relationship_id", "")) == state.relationship_id:
|
||||
raw = entry
|
||||
break
|
||||
relationships.append(_merge_legacy_changes(raw, original, current) if not original.is_empty() else current)
|
||||
var candidates := _threshold_candidates.duplicate(true)
|
||||
candidates.sort_custom(
|
||||
func(a: Dictionary, b: Dictionary) -> bool:
|
||||
return String(a.get(&"candidate_id", "")) < String(b.get(&"candidate_id", ""))
|
||||
)
|
||||
return {
|
||||
&"version": SNAPSHOT_VERSION,
|
||||
&"relationships": relationships,
|
||||
&"threshold_candidates": candidates,
|
||||
&"next_candidate_sequence": _next_candidate_sequence,
|
||||
}
|
||||
var legacy := _legacy_raw.duplicate(true)
|
||||
legacy[&"version"] = 1
|
||||
# Preserve input collection order as well as every untouched raw field.
|
||||
if _legacy_raw.has(&"relationships"):
|
||||
var ordered: Array[Dictionary] = []
|
||||
for raw: Dictionary in _legacy_raw[&"relationships"]:
|
||||
for entry: Dictionary in relationships:
|
||||
if _value(entry, &"relationship_id", "") == _value(raw, &"relationship_id", ""):
|
||||
ordered.append(entry)
|
||||
for entry: Dictionary in relationships:
|
||||
if not ordered.has(entry):
|
||||
ordered.append(entry)
|
||||
relationships = ordered
|
||||
if legacy.get(&"relationships") != relationships:
|
||||
legacy[&"relationships"] = relationships
|
||||
var current_candidates := _threshold_candidates.duplicate(true) if not _legacy_raw.is_empty() else candidates
|
||||
if legacy.get(&"threshold_candidates") != current_candidates:
|
||||
legacy[&"threshold_candidates"] = current_candidates
|
||||
legacy[&"next_candidate_sequence"] = _next_candidate_sequence
|
||||
var result := _container_raw.duplicate(true)
|
||||
result[&"version"] = SNAPSHOT_VERSION
|
||||
result[&"legacy"] = legacy
|
||||
# Compatibility projection for existing snapshot consumers; the nested v1
|
||||
# record remains lossless, including extension names reserved by v2.
|
||||
if result.get(&"relationships") != relationships:
|
||||
result[&"relationships"] = relationships.duplicate(true)
|
||||
var originals: Array[Dictionary] = []
|
||||
for payload: Dictionary in _original_records.values():
|
||||
originals.append(payload.duplicate(true))
|
||||
if result.get(&"original_relationships") != originals:
|
||||
result[&"original_relationships"] = originals
|
||||
return result
|
||||
|
||||
|
||||
static func _merge_legacy_changes(raw: Dictionary, baseline: Dictionary, current: Dictionary) -> Dictionary:
|
||||
var result := raw.duplicate(true)
|
||||
for key: Variant in baseline:
|
||||
if not current.has(key):
|
||||
result.erase(key)
|
||||
for key: Variant in current:
|
||||
if baseline.has(key) and current[key] == baseline[key]:
|
||||
continue
|
||||
if current[key] is Dictionary and baseline.get(key) is Dictionary and result.get(key) is Dictionary:
|
||||
result[key] = _merge_legacy_changes(result[key], baseline[key], current[key])
|
||||
else:
|
||||
result[key] = current[key]
|
||||
return result
|
||||
|
||||
|
||||
func restore_snapshot(snapshot: Dictionary) -> bool:
|
||||
var parsed := _parse_snapshot(snapshot)
|
||||
var version: Variant = _value(snapshot, &"version", null)
|
||||
if not version is int or version not in [1, SNAPSHOT_VERSION]:
|
||||
last_error = "Unsupported Relationship snapshot version."
|
||||
return false
|
||||
var legacy: Variant = snapshot if version == 1 else _value(snapshot, &"legacy", null)
|
||||
var originals: Variant = [] if version == 1 else _value(snapshot, &"original_relationships", null)
|
||||
if not legacy is Dictionary or not originals is Array:
|
||||
last_error = "Malformed Relationship snapshot container."
|
||||
return false
|
||||
if version == SNAPSHOT_VERSION and _value(snapshot, &"relationships", null) != _value(legacy, &"relationships", null):
|
||||
last_error = "Legacy compatibility projection disagrees with its source."
|
||||
return false
|
||||
var original_records: Dictionary = {}
|
||||
for raw: Variant in originals:
|
||||
var state := OriginalStateType.from_snapshot(raw)
|
||||
if state == null or original_records.has(state.subject_id()):
|
||||
last_error = "Invalid or duplicate original relationship."
|
||||
return false
|
||||
if not _original_identity_validator.is_valid() or not bool(_original_identity_validator.call(state.create_snapshot())):
|
||||
last_error = "Invalid original relationship identity reference."
|
||||
return false
|
||||
original_records[state.subject_id()] = state.create_snapshot()
|
||||
var parsed := _parse_snapshot(legacy)
|
||||
if not bool(parsed.get(&"ok", false)):
|
||||
last_error = String(parsed.get(&"error", "Invalid Relationship snapshot."))
|
||||
return false
|
||||
@@ -370,12 +483,18 @@ func restore_snapshot(snapshot: Dictionary) -> bool:
|
||||
_pair_index = parsed[&"pair_index"]
|
||||
_threshold_candidates = parsed[&"threshold_candidates"]
|
||||
_next_candidate_sequence = int(parsed[&"next_candidate_sequence"])
|
||||
_original_records = original_records
|
||||
_legacy_raw = legacy.duplicate(true)
|
||||
_container_raw = snapshot.duplicate(true) if version == SNAPSHOT_VERSION else {}
|
||||
_legacy_baselines = {}
|
||||
for state: RelationshipState in get_relationships():
|
||||
_legacy_baselines[state.relationship_id] = state.create_snapshot()
|
||||
last_error = ""
|
||||
return true
|
||||
|
||||
|
||||
func _parse_snapshot(snapshot: Dictionary) -> Dictionary:
|
||||
if int(_value(snapshot, &"version", 0)) != SNAPSHOT_VERSION:
|
||||
if not _value(snapshot, &"version", null) is int or _value(snapshot, &"version", null) != 1:
|
||||
return {&"ok": false, &"error": "Unsupported Relationship snapshot version."}
|
||||
var raw_relationships: Variant = _value(snapshot, &"relationships", null)
|
||||
var raw_candidates: Variant = _value(snapshot, &"threshold_candidates", null)
|
||||
@@ -384,7 +503,7 @@ func _parse_snapshot(snapshot: Dictionary) -> Dictionary:
|
||||
var relationships: Dictionary = {}
|
||||
var pair_index: Dictionary = {}
|
||||
for raw_state: Variant in raw_relationships:
|
||||
if not raw_state is Dictionary:
|
||||
if not raw_state is Dictionary or not _legacy_types_valid(raw_state):
|
||||
return {&"ok": false, &"error": "Relationship snapshot entry is malformed."}
|
||||
var state := RelationshipStateType.from_snapshot(raw_state)
|
||||
if not state.is_valid() or relationships.has(state.relationship_id):
|
||||
@@ -398,6 +517,9 @@ func _parse_snapshot(snapshot: Dictionary) -> Dictionary:
|
||||
for raw_candidate: Variant in raw_candidates:
|
||||
if not raw_candidate is Dictionary:
|
||||
return {&"ok": false, &"error": "Threshold candidate is malformed."}
|
||||
for key: String in ["candidate_id", "relationship_id"]:
|
||||
if not (raw_candidate.get(key) is String or raw_candidate.get(key) is StringName):
|
||||
return {&"ok": false, &"error": "Threshold candidate identity type is invalid."}
|
||||
var candidate: Dictionary = raw_candidate.duplicate(true)
|
||||
var candidate_id := StringName(candidate.get(&"candidate_id", candidate.get("candidate_id", "")))
|
||||
var relationship_id := StringName(candidate.get(&"relationship_id", candidate.get("relationship_id", "")))
|
||||
@@ -405,6 +527,8 @@ func _parse_snapshot(snapshot: Dictionary) -> Dictionary:
|
||||
return {&"ok": false, &"error": "Threshold candidate reference is invalid."}
|
||||
candidate_ids[candidate_id] = true
|
||||
candidates.append(candidate)
|
||||
if not _value(snapshot, &"next_candidate_sequence", null) is int:
|
||||
return {&"ok": false, &"error": "Relationship candidate sequence type is invalid."}
|
||||
var next_sequence := int(_value(snapshot, &"next_candidate_sequence", 0))
|
||||
if next_sequence < 1:
|
||||
return {&"ok": false, &"error": "Relationship candidate sequence is invalid."}
|
||||
@@ -417,6 +541,44 @@ func _parse_snapshot(snapshot: Dictionary) -> Dictionary:
|
||||
}
|
||||
|
||||
|
||||
static func _legacy_types_valid(raw: Dictionary) -> bool:
|
||||
for key: String in ["relationship_id", "actor_a_id", "actor_b_id"]:
|
||||
if not (raw.get(key) is String or raw.get(key) is StringName):
|
||||
return false
|
||||
for key: String in ["shield_max", "shield_current", "last_event_at", "daily_attack_count", "attack_day", "metric_delta_day"]:
|
||||
if raw.has(key) and not raw[key] is int:
|
||||
return false
|
||||
for key: String in ["metrics", "action_counts", "daily_metric_deltas", "flags", "crossed_thresholds"]:
|
||||
if raw.has(key) and not raw[key] is Dictionary:
|
||||
return false
|
||||
for key: String in ["trust", "respect", "affection", "fear", "dependence", "affinity", "obligation"]:
|
||||
var metrics: Dictionary = raw.get("metrics", raw)
|
||||
if metrics.has(key) and not metrics[key] is int:
|
||||
return false
|
||||
for key: String in ["action_counts", "daily_metric_deltas"]:
|
||||
for value: Variant in raw.get(key, {}).values():
|
||||
if not value is int:
|
||||
return false
|
||||
for key: String in ["action_counts", "flags"]:
|
||||
for identity: Variant in raw.get(key, {}):
|
||||
if not (identity is String or identity is StringName):
|
||||
return false
|
||||
for value: Variant in raw.get("flags", {}).values():
|
||||
if not value is bool:
|
||||
return false
|
||||
for value: Variant in raw.get("crossed_thresholds", {}).values():
|
||||
if not value is Array:
|
||||
return false
|
||||
for threshold: Variant in value:
|
||||
if not threshold is int:
|
||||
return false
|
||||
if raw.has("route_id") and not (raw["route_id"] is String or raw["route_id"] is StringName):
|
||||
return false
|
||||
if raw.has("route_lock") and not (raw["route_lock"] is int or raw["route_lock"] is String or raw["route_lock"] is StringName):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func _register_threshold_candidate(
|
||||
state: RelationshipState,
|
||||
metric_id: StringName,
|
||||
|
||||
Reference in New Issue
Block a user