Talk:Topic

From the CreationKit Wiki
Jump to navigation Jump to search

Reverse engineering[edit source]

I'm looking into precisely how infos are randomized. Findings so far:

  • If a Topic Info's "Hours Until Reset" cooldown is active, then the selection algorithm acts as though that Topic Info doesn't even exist. This means that flags like "Random End" won't influence randomization.
  • Ditto for if the info is a deleted record, or if it's flagged as "Say Once" but doesn't belong to the Player Dialogue tab.

Using Lua as pseudocode:

function topic:pick_info(
   result,  -- C++ bool&; Lua can't do that so let's imagine it's a table like { _value = ... }
   subject, -- ref
   target,  -- ref
   arg4,    -- bool
   arg5,    -- optional topic
   arg6     -- optional unknown struct (intrusive linked list; possibly related to ExtraSayToTopicInfo)
)
   result._value = false
   if self:is_deleted() then
      return nil
   end
   local infos_done_before_repeating = {}
   local final_candidate_topic_infos = {}
   local random_ended                = false
   local last_seen_candidate         = nil
   if  self.owning_quest
   and (self.owning_quest.quest_flags & 1) -- started
   and !(self.owning_quest.quest_flags & (1 << 7)) -- Stage Wait
   and !self.owning_quest.promote_task
   then
      if self.owning_quest.dialogue_conditions:IsTrue(subject, target, self.owning_quest) then
         for _, info in ipairs(self.infos) do
            local esp13 = { _value = false } -- in C++, another raw bool that gets passed by reference
            if info and (not info:is_in_cooldown()) then
               if info:check_conditions(esp13, subject, target) then
                  if info.is_random_end then
                     random_ended = true
                  end
                  if arg4 or (arg5 and TOPIC_SUBTYPE_DEFINITIONS[arg5.subtype].unk04 == 0) then
                     while arg6 then
                        if not arg6.next and not arg6.data then
                           break
                        end
                        if arg6.data.unk0C == info then
                           goto after_gather_for_randomization
                        end
                        if arg6.data.unk14 ~= self.owning_quest then
                           goto continue
                        end
                        if arg6.data.unk10 == self then
                           goto after_gather_for_randomization
                        end
                        ::continue::
                        arg6 = arg6.next
                     end
                  end
                  if not info.is_random then
                     if #final_candidate_topic_infos == 0 then
                        return info
                     end
                     break
                  end
                  if this.do_all_before_repeating and info.info_flag_15 then
                     infos_done_before_repeating[#infos_done_before_repeating + 1] = info
                  else
                     final_candidate_topic_infos[#final_candidate_topic_infos + 1] = info
                  end
               end
            end
            ::after_gather_for_randomization::
            if (not arg4) and esp13._value then
               --
               -- Boolean `esp13` is false if the info is deleted, a misplaced "Say Once" info, etc..
               -- I'm not sure what happens to this boolean otherwise, i.e. if the info is able to 
               -- try its conditions but then the conditions don't match.
               --
               result._value = true
               last_seen_candidate = info
            end
            if random_ended then
               break
            end
         end
      end
   end
   if self.do_all_before_repeating and #final_candidate_topic_infos == 0 then
      for _, info in ipairs(infos_done_before_repeating) do
         info.info_flag_15 = false -- sentinel bit 0x8000 in the info flags mask
      end
      final_candidate_topic_infos = infos_done_before_repeating
   end
   if #final_candidate_topic_infos > 0 then
      if #final_candidate_topic_infos == 1 then
         return final_candidate_topic_infos[0]
      end
      -- Pick a random candidate:
      return final_candidate_topic_infos[math.random(#final_candidate_topic_infos)]
   end
   if arg4 then
      return nil
   end
   return last_seen_candidate
end


DavidJCobb (talk) 01:29, 8 April 2026 (EDT)

In particular, static analysis suggests that if a Topic Info's conditions don't test as true, then the presence of the "Random End" flag or the absence of the "Random" flag on that info don't affect anything. For example, given the following infos:
Caption text
Conditions met Flags Text
Yes Random This is Line A
Yes Random This is Line B
No Random, Random End This is Line C
Yes Random This is Line D
No This is Line E
Yes Random, Random End This is Line F
Manual examination of the compiled code suggests that it would be possible for an NPC to say any of A, B, D, and F; the intervening Random-End and non-random lines fail to break the topic into separately randomized groups because those lines' conditions aren't met and their flags therefore do not influence the random selection. This is very counterintuitive, particularly given how infos are documented to work on the main page, but it aligns with the behavior I've been seeing while working on a mod with lots of infos and very complex info conditions. DavidJCobb (talk) 01:52, 8 April 2026 (EDT)
Just to really drive the point home, here's a screenshot of that table with some scribbling on it to more clearly denote the randomized line groups you would expect versus those that the game actually uses [per my reverse engineering and what behaviors I'm seeing in-game] given the condition results per line:
Topic Random End jank example for talk page.png
As for how this behavior arose? You'd only run into problems with this if you have a topic consisting solely of groups of randomized lines, wherein some groups have overlapping conditions, not all lines within a group have the exact same conditions, and multiple groups have their Random End lines fail to match conditions. The case that led me to run into this was a topic that checks an NPC's location and reports it to the player, with variable wording based on the NPC's gender. The topic contained one group of randomized lines per location, but lines within a group were interleaved by gender (i.e. male/female/male/female), such that the "Random End" flag was placed on female lines, and therefore asking about a male NPC could cause you to hear lines from multiple groups (including the "I don't know where he is" group). It's quite conceivable that the behavior I'm documenting here -- the flags being ignored -- is literally just a bug that Bethesda never ran into due to how specific things have to get for this to actually cause problems.
DavidJCobb (talk) 02:27, 8 April 2026 (EDT)