feat: deliver emergent Jajce commitments

This commit is contained in:
Rijad Zuzo
2026-08-12 21:15:58 +02:00
parent dc0f59fcf0
commit 6b50580598
28 changed files with 2145 additions and 206 deletions
@@ -0,0 +1,111 @@
class_name ConversationActStateRecord
extends RefCounted
const SCHEMA_VERSION := 1
const MAX_TOPICS := 8
var data: Dictionary
func _init(record_data: Dictionary = {}) -> void:
data = record_data.duplicate(true)
static func create(
act_id: int,
tick: int,
speaker: WorldEntityRef,
listener: WorldEntityRef,
intent_id: StringName,
topic_ids: Array[StringName] = []
) -> ConversationActStateRecord:
if (
act_id < 0
or tick < 0
or speaker == null
or listener == null
or not speaker.is_valid()
or not listener.is_valid()
or speaker.equals(listener)
or not ConversationIntentIds.is_supported(intent_id)
or topic_ids.size() > MAX_TOPICS
):
return null
var normalized_topics: Array[String] = []
var seen: Dictionary = {}
for topic_id in topic_ids:
if topic_id.is_empty() or seen.has(topic_id):
return null
seen[topic_id] = true
normalized_topics.append(String(topic_id))
return (
ConversationActStateRecord
. new(
{
"schema_version": SCHEMA_VERSION,
"act_id": act_id,
"tick": tick,
"speaker": speaker.to_dictionary(),
"listener": listener.to_dictionary(),
"intent_id": String(intent_id),
"topic_ids": normalized_topics,
}
)
)
static func from_dictionary(record_data: Dictionary) -> ConversationActStateRecord:
if (
int(record_data.get("schema_version", -1)) != SCHEMA_VERSION
or not record_data.has_all(
["act_id", "tick", "speaker", "listener", "intent_id", "topic_ids"]
)
or not record_data["speaker"] is Dictionary
or not record_data["listener"] is Dictionary
or not record_data["topic_ids"] is Array
):
return null
var topics: Array[StringName] = []
for topic in record_data["topic_ids"]:
if not topic is String and not topic is StringName:
return null
topics.append(StringName(topic))
return create(
int(record_data["act_id"]),
int(record_data["tick"]),
WorldEntityRef.from_dictionary(record_data["speaker"]),
WorldEntityRef.from_dictionary(record_data["listener"]),
StringName(record_data["intent_id"]),
topics
)
func get_act_id() -> int:
return int(data["act_id"])
func get_tick() -> int:
return int(data["tick"])
func get_speaker() -> WorldEntityRef:
return WorldEntityRef.from_dictionary(data["speaker"])
func get_listener() -> WorldEntityRef:
return WorldEntityRef.from_dictionary(data["listener"])
func get_intent_id() -> StringName:
return StringName(data["intent_id"])
func get_topic_ids() -> Array[StringName]:
var result: Array[StringName] = []
for topic in data["topic_ids"]:
result.append(StringName(topic))
return result
func to_dictionary() -> Dictionary:
return data.duplicate(true)
@@ -0,0 +1 @@
uid://b4iruxxxc2g2j
@@ -154,6 +154,16 @@ func set_tracking_preference(preference: StringName) -> bool:
return true
func select_alternative(alternative_id: StringName) -> bool:
if not is_active() or alternative_id.is_empty():
return false
var selected := get_selected_alternative_id()
if not selected.is_empty() and selected != alternative_id:
return false
data["selected_alternative_id"] = String(alternative_id)
return true
func archive(closed_tick: int) -> bool:
return close(STATUS_ARCHIVED, closed_tick)
+121 -26
View File
@@ -2,7 +2,7 @@ class_name SimulationStateRecord
extends RefCounted
const SCHEMA_NAME := "the_steward.simulation"
const SCHEMA_VERSION := 14
const SCHEMA_VERSION := 15
const LEGACY_SCHEMA_VERSION := 1
const EVENT_LEGACY_SCHEMA_VERSION := 2
const RELATIONSHIP_LEGACY_SCHEMA_VERSION := 3
@@ -15,6 +15,7 @@ const ROUTINE_LEGACY_SCHEMA_VERSION := 10
const PLAYER_RELATIONSHIP_SCHEMA_VERSION := 11
const PLAYER_NEEDS_SCHEMA_VERSION := 12
const CONFLICT_SCHEMA_VERSION := 13
const PRE_EMERGENT_SCHEMA_VERSION := 14
const PREVIOUS_SCHEMA_VERSION := 7
var simulation: Dictionary
@@ -27,6 +28,10 @@ var economic_events: Array[EconomicEventRecord] = []
var relationships: Array[RelationshipStateRecord] = []
var event_knowledge: Array[KnownEventStateRecord] = []
var opportunities: Array[OpportunityStateRecord] = []
var situations: Array[SituationStateRecord] = []
var quest_journal: Array[QuestJournalEntryStateRecord] = []
var commitments: Array[CommitmentStateRecord] = []
var conversation_history: Array[ConversationActStateRecord] = []
var player: PlayerStateRecord
var combatants: Array[CombatantStateRecord] = []
var factions: Array[FactionStateRecord] = []
@@ -58,6 +63,18 @@ func to_dictionary() -> Dictionary:
var opportunity_data: Array[Dictionary] = []
for opportunity_record in opportunities:
opportunity_data.append(opportunity_record.to_dictionary())
var situation_data: Array[Dictionary] = []
for situation_record in situations:
situation_data.append(situation_record.to_dictionary())
var journal_data: Array[Dictionary] = []
for journal_record in quest_journal:
journal_data.append(journal_record.to_dictionary())
var commitment_data: Array[Dictionary] = []
for commitment_record in commitments:
commitment_data.append(commitment_record.to_dictionary())
var conversation_history_data: Array[Dictionary] = []
for conversation_act in conversation_history:
conversation_history_data.append(conversation_act.to_dictionary())
var combatant_data: Array[Dictionary] = []
for combatant_record in combatants:
combatant_data.append(combatant_record.to_dictionary())
@@ -78,6 +95,10 @@ func to_dictionary() -> Dictionary:
"relationships": relationship_data,
"event_knowledge": knowledge_data,
"opportunities": opportunity_data,
"situations": situation_data,
"quest_journal": journal_data,
"commitments": commitment_data,
"conversation_history": conversation_history_data,
"player": player.to_dictionary(),
"combatants": combatant_data,
"factions": faction_data
@@ -115,6 +136,7 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
PLAYER_RELATIONSHIP_SCHEMA_VERSION,
PLAYER_NEEDS_SCHEMA_VERSION,
CONFLICT_SCHEMA_VERSION,
PRE_EMERGENT_SCHEMA_VERSION,
]
):
record_data = _migrate_legacy(record_data, version)
@@ -134,6 +156,10 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
"relationships",
"event_knowledge",
"opportunities",
"situations",
"quest_journal",
"commitments",
"conversation_history",
"player",
"combatants",
"factions",
@@ -157,6 +183,10 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
"wander_random_streams",
"next_event_id",
"next_opportunity_id",
"next_situation_id",
"next_journal_entry_id",
"next_commitment_id",
"next_conversation_act_id",
]
)
):
@@ -166,6 +196,10 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
var saved_clock_accumulator := float(simulation_data["clock_accumulator"])
var saved_clock_elapsed_ticks := int(simulation_data["clock_elapsed_ticks"])
var saved_next_opportunity_id := int(simulation_data["next_opportunity_id"])
var saved_next_situation_id := int(simulation_data["next_situation_id"])
var saved_next_journal_entry_id := int(simulation_data["next_journal_entry_id"])
var saved_next_commitment_id := int(simulation_data["next_commitment_id"])
var saved_next_conversation_act_id := int(simulation_data["next_conversation_act_id"])
if (
not is_finite(saved_tick_interval)
or saved_tick_interval <= 0.0
@@ -174,6 +208,10 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
or saved_clock_accumulator < 0.0
or saved_clock_elapsed_ticks < 0
or saved_next_opportunity_id < 0
or saved_next_situation_id < 0
or saved_next_journal_entry_id < 0
or saved_next_commitment_id < 0
or saved_next_conversation_act_id < 0
):
return null
if simulation_data.has("cycle_duration_seconds"):
@@ -204,6 +242,10 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
var relationship_data = record_data["relationships"]
var knowledge_data = record_data["event_knowledge"]
var opportunity_data = record_data["opportunities"]
var situation_data = record_data["situations"]
var journal_data = record_data["quest_journal"]
var commitment_data = record_data["commitments"]
var conversation_history_data = record_data["conversation_history"]
if (
not npc_data is Array
or not animal_data is Array
@@ -213,6 +255,10 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
or not relationship_data is Array
or not knowledge_data is Array
or not opportunity_data is Array
or not situation_data is Array
or not journal_data is Array
or not commitment_data is Array
or not conversation_history_data is Array
):
return null
@@ -425,7 +471,10 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
return null
var knower_id := known_event_record.get_knower_id()
var known_event_id := known_event_record.get_event_id()
if not npc_ids.has(knower_id) or not event_ids.has(known_event_id):
if (
(knower_id != SimulationIds.PLAYER_ACTOR_ID and not npc_ids.has(knower_id))
or not event_ids.has(known_event_id)
):
return null
var knowledge_key := "%d:%d" % [knower_id, known_event_id]
if knowledge_keys.has(knowledge_key):
@@ -448,29 +497,28 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
return null
var observer_id := relationship_record.get_observer_id()
var subject_id := relationship_record.get_subject_id()
var is_player_observer := observer_id == SimulationIds.PLAYER_ACTOR_ID
var is_player_subject := subject_id == SimulationIds.PLAYER_ACTOR_ID
if not npc_ids.has(observer_id):
if not is_player_observer and not npc_ids.has(observer_id):
return null
if not is_player_subject and not npc_ids.has(subject_id):
return null
var relationship_key := "%d:%d" % [observer_id, subject_id]
if relationship_keys.has(relationship_key):
return null
var cause_event_id := relationship_record.get_last_trust_cause_event_id()
if (
cause_event_id != RelationshipStateRecord.NO_CAUSE_EVENT
and not event_ids.has(cause_event_id)
):
return null
if (
cause_event_id != RelationshipStateRecord.NO_CAUSE_EVENT
and not knowledge_keys.has("%d:%d" % [observer_id, cause_event_id])
):
return null
if cause_event_id != RelationshipStateRecord.NO_CAUSE_EVENT:
var cause_event := event_records_by_id[cause_event_id] as EconomicEventRecord
if not _is_valid_relationship_cause(cause_event, subject_id, is_player_subject):
for dimension in RelationshipStateRecord.DIMENSIONS:
var cause_event_id := relationship_record.get_last_cause_event_id(dimension)
if cause_event_id == RelationshipStateRecord.NO_CAUSE_EVENT:
continue
if (
not event_ids.has(cause_event_id)
or not knowledge_keys.has("%d:%d" % [observer_id, cause_event_id])
):
return null
if dimension == RelationshipStateRecord.DIMENSION_TRUST:
var cause_event := event_records_by_id[cause_event_id] as EconomicEventRecord
if not _is_valid_relationship_cause(cause_event, subject_id, is_player_subject):
return null
relationship_keys[relationship_key] = true
record.relationships.append(relationship_record)
@@ -515,6 +563,36 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
if int(record.simulation["next_opportunity_id"]) <= highest_opportunity_id:
return null
var emergent_records: Variant = EmergentStateValidator.parse(
situation_data, journal_data, commitment_data, simulation_data, npc_ids, event_ids
)
if emergent_records == null:
return null
record.situations.assign(emergent_records["situations"])
record.quest_journal.assign(emergent_records["quest_journal"])
record.commitments.assign(emergent_records["commitments"])
var conversation_act_ids: Dictionary = {}
var highest_conversation_act_id := -1
for item in conversation_history_data:
if not item is Dictionary:
return null
var conversation_act := ConversationActStateRecord.from_dictionary(item)
if (
conversation_act == null
or conversation_act_ids.has(conversation_act.get_act_id())
or conversation_act.get_tick() > saved_tick_count
or not EmergentStateValidator.is_valid_actor(conversation_act.get_speaker(), npc_ids)
or not EmergentStateValidator.is_valid_actor(conversation_act.get_listener(), npc_ids)
):
return null
conversation_act_ids[conversation_act.get_act_id()] = true
highest_conversation_act_id = maxi(
highest_conversation_act_id, conversation_act.get_act_id()
)
record.conversation_history.append(conversation_act)
if saved_next_conversation_act_id <= highest_conversation_act_id:
return null
return record
@@ -579,16 +657,21 @@ static func _is_valid_knowledge_provenance(
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED:
if knower_id == actor_id:
return false
var source_npc_id := known_event.get_source_npc_id()
if not npc_ids.has(source_npc_id):
var source_actor_id := known_event.get_source_actor_id()
if (
source_actor_id != SimulationIds.PLAYER_ACTOR_ID
and not npc_ids.has(source_actor_id)
):
return false
var source_method := known_event.get_source_acquisition_method()
if (
(source_method == SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED)
!= (source_npc_id == actor_id)
!= (source_actor_id == actor_id)
):
return false
return true
if source_method not in KnownEventStateRecord.DIRECT_ACQUISITION_METHODS:
return false
return known_event.get_hop_count() == 1
return false
@@ -774,12 +857,13 @@ static func _is_valid_home_damaged_trigger(
static func _is_valid_relationship_cause(
cause_event: EconomicEventRecord, subject_id: int, is_player_subject: bool
) -> bool:
var event_type := StringName(cause_event.data["event_type"])
if EmergentStateValidator.is_valid_commitment_relationship_cause(cause_event, subject_id):
return true
if not is_player_subject:
return (
int(cause_event.data["actor_id"]) == subject_id
and (
StringName(cause_event.data["event_type"]) == SimulationIds.EVENT_STORAGE_DEPOSITED
)
and (event_type == SimulationIds.EVENT_STORAGE_DEPOSITED)
and StringName(cause_event.data["item_id"]) == SimulationIds.RESOURCE_FOOD
and float(cause_event.data["amount"]) > 0.0
)
@@ -789,7 +873,7 @@ static func _is_valid_relationship_cause(
return false
if float(cause_event.data["amount"]) <= 0.0:
return false
if StringName(cause_event.data["event_type"]) == SimulationIds.EVENT_STORAGE_DEPOSITED:
if event_type == SimulationIds.EVENT_STORAGE_DEPOSITED:
return (
StringName(cause_event.data["source_id"]) == SimulationIds.PLAYER_INVENTORY_ID
and (
@@ -798,7 +882,7 @@ static func _is_valid_relationship_cause(
)
)
return (
StringName(cause_event.data["event_type"]) == SimulationIds.EVENT_RESOURCE_EXTRACTED
event_type == SimulationIds.EVENT_RESOURCE_EXTRACTED
and StringName(cause_event.data["destination_id"]) == SimulationIds.STORAGE_VILLAGE_PANTRY
)
@@ -848,6 +932,16 @@ static func _is_valid_supply_resolution(
static func _migrate_legacy(legacy_data: Dictionary, version: int) -> Dictionary:
var migrated := legacy_data.duplicate(true)
migrated["schema_version"] = SCHEMA_VERSION
migrated["situations"] = []
migrated["quest_journal"] = []
migrated["commitments"] = []
migrated["conversation_history"] = []
var emergent_simulation_data: Dictionary = migrated.get("simulation", {})
emergent_simulation_data["next_situation_id"] = 0
emergent_simulation_data["next_journal_entry_id"] = 0
emergent_simulation_data["next_commitment_id"] = 0
emergent_simulation_data["next_conversation_act_id"] = 0
migrated["simulation"] = emergent_simulation_data
if not migrated.has("player"):
migrated["player"] = PlayerStateRecord.create_default().to_dictionary()
if version < CONFLICT_SCHEMA_VERSION:
@@ -927,6 +1021,7 @@ static func _migrate_legacy(legacy_data: Dictionary, version: int) -> Dictionary
PLAYER_RELATIONSHIP_SCHEMA_VERSION,
PLAYER_NEEDS_SCHEMA_VERSION,
CONFLICT_SCHEMA_VERSION,
PRE_EMERGENT_SCHEMA_VERSION,
]
):
migrated["opportunities"] = []
+5 -1
View File
@@ -28,6 +28,10 @@ static func create(
return null
if trigger_event_id < NO_EVENT_ID:
return null
var normalized_context := context.duplicate(true)
for integer_key in ["event_id", "opportunity_id"]:
if normalized_context.has(integer_key):
normalized_context[integer_key] = int(normalized_context[integer_key])
return (
SituationStateRecord
. new(
@@ -43,7 +47,7 @@ static func create(
"resolution_event_id": NO_EVENT_ID,
"closed_tick": -1,
"close_reason": "",
"context": context.duplicate(true),
"context": normalized_context,
}
)
)
@@ -0,0 +1,255 @@
class_name EmergentStateValidator
extends RefCounted
static func parse(
situation_data: Array,
journal_data: Array,
commitment_data: Array,
simulation_data: Dictionary,
npc_ids: Dictionary,
event_ids: Dictionary
) -> Variant:
var current_tick := int(simulation_data["tick_count"])
var next_situation_id := int(simulation_data["next_situation_id"])
var next_journal_entry_id := int(simulation_data["next_journal_entry_id"])
var next_commitment_id := int(simulation_data["next_commitment_id"])
var situation_system := SituationSystem.create_default()
var situations_by_id: Dictionary = {}
var situations: Array[SituationStateRecord] = []
if not _parse_situations(
situation_data,
current_tick,
next_situation_id,
npc_ids,
event_ids,
situation_system,
situations_by_id,
situations
):
return null
var journal: Array[QuestJournalEntryStateRecord] = []
if not _parse_journal(
journal_data,
current_tick,
next_journal_entry_id,
situation_system,
situations_by_id,
journal
):
return null
var commitments: Array[CommitmentStateRecord] = []
if not _parse_commitments(
commitment_data,
current_tick,
next_commitment_id,
npc_ids,
event_ids,
situation_system,
situations_by_id,
commitments
):
return null
return {
"situations": situations,
"quest_journal": journal,
"commitments": commitments,
}
static func _parse_situations(
serialized: Array,
current_tick: int,
next_id: int,
npc_ids: Dictionary,
event_ids: Dictionary,
system: SituationSystem,
by_id: Dictionary,
result: Array[SituationStateRecord]
) -> bool:
var highest_id := -1
for item in serialized:
if not item is Dictionary:
return false
var record := SituationStateRecord.from_dictionary(item)
if record == null:
return false
var situation_id := record.get_situation_id()
var definition := system.get_definition(record.get_definition_id())
if by_id.has(situation_id) or definition == null:
return false
if (
record.get_created_tick() > current_tick
or (not record.is_active() and record.get_closed_tick() > current_tick)
):
return false
var trigger_event_id := record.get_trigger_event_id()
if (
trigger_event_id != SituationStateRecord.NO_EVENT_ID
and not event_ids.has(trigger_event_id)
):
return false
var resolution_event_id := record.get_resolution_event_id()
if (
resolution_event_id != SituationStateRecord.NO_EVENT_ID
and (not event_ids.has(resolution_event_id) or resolution_event_id <= trigger_event_id)
):
return false
var alternative_id := record.get_selected_alternative_id()
if not alternative_id.is_empty() and definition.get_alternative(alternative_id) == null:
return false
var context := record.get_context()
if context.has("interested_npc_id"):
var interested_id_text := String(context["interested_npc_id"])
if (
not interested_id_text.is_valid_int()
or not npc_ids.has(interested_id_text.to_int())
):
return false
by_id[situation_id] = record
highest_id = maxi(highest_id, situation_id)
result.append(record)
if next_id <= highest_id:
return false
return system.restore(result, next_id)
static func _parse_journal(
serialized: Array,
current_tick: int,
next_id: int,
situation_system: SituationSystem,
situations_by_id: Dictionary,
result: Array[QuestJournalEntryStateRecord]
) -> bool:
var ids: Dictionary = {}
var situation_ids: Dictionary = {}
var highest_id := -1
for item in serialized:
if not item is Dictionary:
return false
var record := QuestJournalEntryStateRecord.from_dictionary(item)
if record == null:
return false
var entry_id := record.get_entry_id()
var situation := situations_by_id.get(record.get_situation_id()) as SituationStateRecord
if (
ids.has(entry_id)
or situation_ids.has(record.get_situation_id())
or situation == null
or record.get_definition_id() != situation.get_definition_id()
or record.get_discovered_tick() < situation.get_created_tick()
or record.get_discovered_tick() > current_tick
or (not record.is_active() and record.get_closed_tick() > current_tick)
):
return false
var definition := situation_system.get_definition(record.get_definition_id())
var alternative_id := record.get_selected_alternative_id()
if (
not alternative_id.is_empty()
and (
definition == null
or definition.get_alternative(alternative_id) == null
or situation.get_selected_alternative_id() != alternative_id
)
):
return false
ids[entry_id] = true
situation_ids[record.get_situation_id()] = true
highest_id = maxi(highest_id, entry_id)
result.append(record)
if next_id <= highest_id:
return false
return QuestJournalSystem.new().restore(result, next_id)
static func _parse_commitments(
serialized: Array,
current_tick: int,
next_id: int,
npc_ids: Dictionary,
event_ids: Dictionary,
situation_system: SituationSystem,
situations_by_id: Dictionary,
result: Array[CommitmentStateRecord]
) -> bool:
var ids: Dictionary = {}
var highest_id := -1
for item in serialized:
if not item is Dictionary:
return false
var record := CommitmentStateRecord.from_dictionary(item)
if record == null:
return false
var commitment_id := record.get_commitment_id()
var situation := situations_by_id.get(record.get_situation_id()) as SituationStateRecord
if (
ids.has(commitment_id)
or situation == null
or record.get_created_tick() < situation.get_created_tick()
or record.get_created_tick() > current_tick
or (not record.is_active() and record.get_closed_tick() > current_tick)
or not is_valid_actor(record.get_debtor(), npc_ids)
or not is_valid_actor(record.get_creditor(), npc_ids)
):
return false
var definition := situation_system.get_definition(situation.get_definition_id())
var alternative: SituationAlternativeDefinition
if definition != null:
alternative = definition.get_alternative(situation.get_selected_alternative_id())
if alternative == null or alternative.commitment_terms != record.get_terms():
return false
var cause_event_id := record.get_cause_event_id()
if (
cause_event_id != CommitmentStateRecord.NO_EVENT_ID
and not event_ids.has(cause_event_id)
):
return false
ids[commitment_id] = true
highest_id = maxi(highest_id, commitment_id)
result.append(record)
if next_id <= highest_id:
return false
return SocialCommitmentSystem.new().restore(result, next_id)
static func is_valid_actor(entity: WorldEntityRef, npc_ids: Dictionary) -> bool:
if entity == null or not entity.is_valid():
return false
if entity.get_entity_type() == WorldEventRecord.ENTITY_TYPE_PLAYER:
return String(entity.get_entity_id()) == str(SimulationIds.PLAYER_ACTOR_ID)
if entity.get_entity_type() != WorldEventRecord.ENTITY_TYPE_NPC:
return false
var entity_id_text := String(entity.get_entity_id())
return entity_id_text.is_valid_int() and npc_ids.has(entity_id_text.to_int())
static func is_valid_commitment_relationship_cause(
cause_event: EconomicEventRecord, subject_id: int
) -> bool:
if cause_event == null:
return false
if (
StringName(cause_event.data["event_type"])
not in [
SimulationIds.EVENT_COMMITMENT_FULFILLED,
SimulationIds.EVENT_COMMITMENT_BROKEN,
]
):
return false
var debtor_data = cause_event.data.get("debtor")
if not debtor_data is Dictionary:
return false
var debtor := WorldEntityRef.from_dictionary(debtor_data)
if debtor == null:
return false
var expected_type := (
WorldEventRecord.ENTITY_TYPE_PLAYER
if subject_id == SimulationIds.PLAYER_ACTOR_ID
else WorldEventRecord.ENTITY_TYPE_NPC
)
return (
debtor.get_entity_type() == expected_type
and String(debtor.get_entity_id()) == str(subject_id)
and int(cause_event.data["actor_id"]) == subject_id
)
@@ -0,0 +1 @@
uid://bolivdac21erj