KnowledgeGrant untagged enum (Fact + Entity variants), ContentEntityRegistry resource, KnowledgeGranted event processing, ContradictionClaim struct with 600-tick window detection in observe_entity. Wires knowledge_grant field in dialogue line selection. Implements D-079, D-083. Closes Q-026. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -127,6 +127,7 @@ pub fn check_contraband_scan(
|
||||
source: KnowledgeSource::DirectObservation { tick: time.tick },
|
||||
state: KnowledgeState::Active,
|
||||
acquired_tick: time.tick,
|
||||
disclosure_blocked: false,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -463,6 +464,7 @@ mod tests {
|
||||
source: KnowledgeSource::DirectObservation { tick: 0 },
|
||||
state: KnowledgeState::Active,
|
||||
acquired_tick: 0,
|
||||
disclosure_blocked: false,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -25,7 +25,11 @@ use crate::simulation::conversation::{display_label_for_role, NpcColorIndex, Npc
|
||||
use crate::content::line_pool::{
|
||||
AccessTier, IndexedDialogueLine, Mood, Situation, Topic, TrustTier,
|
||||
};
|
||||
use crate::content::types::KnowledgeGrant;
|
||||
use crate::content::LinePoolIndexResource;
|
||||
use crate::knowledge::content_registry::ContentEntityRegistry;
|
||||
use crate::knowledge::events::{ProcessedEntityGrant, ProcessedFactGrant, ProcessedKnowledgeGrant};
|
||||
use crate::knowledge::types::{FactId, KnowledgeConfidence, KnowledgeSource, StableId};
|
||||
use crate::knowledge::{EntityRegistry, KnowledgeGraph};
|
||||
use crate::npc::interaction::{InteractionEvent, InteractionEventKind, InteractionMemory};
|
||||
use crate::npc::relationships::{TrustEvent, TrustEventQueue};
|
||||
@@ -422,6 +426,7 @@ pub fn process_talk_interaction(
|
||||
time: Res<SimulationTime>,
|
||||
line_pool: Option<Res<LinePoolIndexResource>>,
|
||||
registry: Res<EntityRegistry>,
|
||||
content_registry: Res<ContentEntityRegistry>,
|
||||
mut rng: ResMut<SimRng>,
|
||||
mut event_queue: ResMut<crate::knowledge::KnowledgeEventQueue>,
|
||||
mut trust_queue: ResMut<TrustEventQueue>,
|
||||
@@ -442,6 +447,7 @@ pub fn process_talk_interaction(
|
||||
Option<&mut InteractionMemory>,
|
||||
Option<&NpcName>,
|
||||
Option<&NpcColorIndex>,
|
||||
Option<&KnowledgeGraph>,
|
||||
)>,
|
||||
) {
|
||||
let Some(line_pool) = line_pool else {
|
||||
@@ -462,8 +468,8 @@ pub fn process_talk_interaction(
|
||||
|
||||
let target = talk_request.target;
|
||||
|
||||
// Look up NPC dialogue profile, mood, interaction history, name, and color (#325)
|
||||
let Ok((profile, mood_opt, mut interaction_mem_opt, npc_name_opt, color_idx_opt)) =
|
||||
// Look up NPC dialogue profile, mood, interaction history, name, color, and KG (#325, D-079)
|
||||
let Ok((profile, mood_opt, mut interaction_mem_opt, npc_name_opt, color_idx_opt, npc_kg_opt)) =
|
||||
npc_query.get_mut(target)
|
||||
else {
|
||||
tracing::debug!(
|
||||
@@ -541,6 +547,19 @@ pub fn process_talk_interaction(
|
||||
|
||||
cooldown.record(&line.id, time.tick);
|
||||
|
||||
// Knowledge grant (D-079): fire at line selection time, server-authoritative.
|
||||
if let Some(grant) = &line.knowledge_grant {
|
||||
emit_knowledge_grant(
|
||||
grant,
|
||||
player_entity,
|
||||
speaker_stable,
|
||||
&content_registry,
|
||||
npc_kg_opt,
|
||||
time.tick,
|
||||
&mut event_queue,
|
||||
);
|
||||
}
|
||||
|
||||
// Emit IncompleteInteraction if overwriting an existing dialogue session
|
||||
if let Some(prev) = active_dialogue_opt {
|
||||
event_queue.push(crate::knowledge::KnowledgeEvent {
|
||||
@@ -594,6 +613,102 @@ pub fn process_talk_interaction(
|
||||
commands.entity(player_entity).remove::<TalkRequest>();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Knowledge grant helper (D-079)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Emit a KnowledgeGranted event for a dialogue line's knowledge_grant field.
|
||||
///
|
||||
/// Called at line selection time (server-authoritative, tick-stamped).
|
||||
/// Source is always `ToldBy { source_id: speaker_stable, tick }`.
|
||||
///
|
||||
/// Fact grants: dropped with tracing::warn! if the granting NPC's KG
|
||||
/// does not contain the fact (D-079 runtime guardrail).
|
||||
/// Entity grants: no guardrail — always emitted if entity_ref resolves.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn emit_knowledge_grant(
|
||||
grant: &KnowledgeGrant,
|
||||
player_entity: Entity,
|
||||
speaker_stable: StableId,
|
||||
content_registry: &ContentEntityRegistry,
|
||||
npc_kg_opt: Option<&KnowledgeGraph>,
|
||||
tick: u64,
|
||||
event_queue: &mut crate::knowledge::KnowledgeEventQueue,
|
||||
) {
|
||||
let source = KnowledgeSource::ToldBy {
|
||||
source_id: speaker_stable,
|
||||
tick,
|
||||
};
|
||||
|
||||
match grant {
|
||||
KnowledgeGrant::Fact { fact_id, confidence } => {
|
||||
let conf = match KnowledgeConfidence::try_from(confidence.as_str()) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
tracing::warn!("KnowledgeGrant confidence parse error: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let fid = FactId(fact_id.clone());
|
||||
// Guardrail: NPC must know this fact to grant it (D-079).
|
||||
let npc_knows = npc_kg_opt
|
||||
.map(|kg| kg.knows_fact(&fid))
|
||||
.unwrap_or(false);
|
||||
if !npc_knows {
|
||||
tracing::warn!(
|
||||
"KnowledgeGrant dropped: NPC {:?} does not know fact '{}' — grant guardrail",
|
||||
speaker_stable,
|
||||
fact_id
|
||||
);
|
||||
return;
|
||||
}
|
||||
event_queue.push(crate::knowledge::KnowledgeEvent {
|
||||
observer: player_entity,
|
||||
tick,
|
||||
event_type: crate::knowledge::KnowledgeEventType::KnowledgeGranted {
|
||||
grant: ProcessedKnowledgeGrant::Fact(ProcessedFactGrant {
|
||||
fact_id: fid,
|
||||
confidence: conf,
|
||||
}),
|
||||
source,
|
||||
},
|
||||
});
|
||||
}
|
||||
KnowledgeGrant::Entity {
|
||||
entity_ref,
|
||||
attributes,
|
||||
confidence,
|
||||
} => {
|
||||
let conf = match KnowledgeConfidence::try_from(confidence.as_str()) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
tracing::warn!("KnowledgeGrant confidence parse error: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let Some(target_id) = content_registry.resolve(entity_ref) else {
|
||||
tracing::warn!(
|
||||
"KnowledgeGrant::Entity dropped: entity_ref '{}' not in ContentEntityRegistry",
|
||||
entity_ref
|
||||
);
|
||||
return;
|
||||
};
|
||||
event_queue.push(crate::knowledge::KnowledgeEvent {
|
||||
observer: player_entity,
|
||||
tick,
|
||||
event_type: crate::knowledge::KnowledgeEventType::KnowledgeGranted {
|
||||
grant: ProcessedKnowledgeGrant::Entity(ProcessedEntityGrant {
|
||||
target_id,
|
||||
attributes: attributes.clone(),
|
||||
confidence: conf,
|
||||
}),
|
||||
source,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// System: process_walk_away (D-064)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1330,6 +1445,7 @@ mod tests {
|
||||
world.init_resource::<SimulationTime>();
|
||||
world.insert_resource(SimRng::new(42));
|
||||
world.init_resource::<EntityRegistry>();
|
||||
world.init_resource::<ContentEntityRegistry>();
|
||||
world.init_resource::<crate::knowledge::KnowledgeEventQueue>();
|
||||
world.init_resource::<TrustEventQueue>();
|
||||
world.init_resource::<crate::simulation::monologue::PostConversationQueue>();
|
||||
@@ -1828,6 +1944,7 @@ mod tests {
|
||||
|
||||
let mut world = setup_dialogue_world();
|
||||
world.init_resource::<KnowledgeEventQueue>();
|
||||
world.init_resource::<crate::knowledge::ContradictionDetectedQueue>();
|
||||
|
||||
let npc = world.spawn_empty().id();
|
||||
let npc_sid = world.resource_mut::<EntityRegistry>().register(npc);
|
||||
|
||||
Reference in New Issue
Block a user