|
| 1 | +# Dialogue Injection |
| 2 | + |
| 3 | +`DialogueInjection` lets you attach an extra choice to an existing vanilla or custom NPC dialogue flow without replacing the whole container. |
| 4 | + |
| 5 | +This is useful when you want to extend an NPC's normal conversation with one additional branch, such as a service unlock, a follow-up action, or a context-specific option that should only appear for a specific character. |
| 6 | + |
| 7 | +## Overview |
| 8 | + |
| 9 | +The injection flow has two parts: |
| 10 | + |
| 11 | +1. Create a `DialogueInjection` that describes where the new choice should be inserted |
| 12 | +2. Register it with `DialogueInjector.Register(...)` |
| 13 | + |
| 14 | +S1API then waits until the matching NPC is available in the world, finds the requested dialogue container and source node, appends the new choice, creates the node link, and wires up your confirmation callback. |
| 15 | + |
| 16 | +```csharp |
| 17 | +using S1API.Dialogues; |
| 18 | + |
| 19 | +DialogueInjector.Register(new DialogueInjection( |
| 20 | + npc: "Philip", |
| 21 | + container: "CasinoDialogue", |
| 22 | + from: "ROOT_NODE_GUID", |
| 23 | + to: "BANKING_NODE_GUID", |
| 24 | + label: "ASK_ABOUT_PAYOUTS", |
| 25 | + text: "Quick question about payouts.", |
| 26 | + onConfirmed: () => |
| 27 | + { |
| 28 | + // Handle the selected option |
| 29 | + })); |
| 30 | +``` |
| 31 | + |
| 32 | +## When to Use It |
| 33 | + |
| 34 | +Use `DialogueInjection` when you want to: |
| 35 | + |
| 36 | +- Add one or more new choices to an existing NPC dialogue tree |
| 37 | +- Reuse an existing container instead of building a full replacement |
| 38 | +- Hook into a known dialogue node and branch into your own follow-up node |
| 39 | +- Attach lightweight interaction logic to a dialogue choice callback |
| 40 | + |
| 41 | +If you need to build a full custom dialogue flow from scratch, start with **[Dialogue System](dialogue-system.md)** instead. |
| 42 | + |
| 43 | +## Constructor Options |
| 44 | + |
| 45 | +`DialogueInjection` supports two ways to target an NPC. |
| 46 | + |
| 47 | +### By NPC ID |
| 48 | + |
| 49 | +Use the string overload when you know the NPC's ID. |
| 50 | + |
| 51 | +```csharp |
| 52 | +var injection = new DialogueInjection( |
| 53 | + npc: "Philip", |
| 54 | + container: "CasinoDialogue", |
| 55 | + from: "ROOT_NODE_GUID", |
| 56 | + to: "BANKING_NODE_GUID", |
| 57 | + label: "ASK_ABOUT_PAYOUTS", |
| 58 | + text: "Quick question about payouts.", |
| 59 | + onConfirmed: OnAskAboutPayouts); |
| 60 | +``` |
| 61 | + |
| 62 | +Internally this maps to `x => x.ID.Equals(npc)`. |
| 63 | + |
| 64 | +### By Predicate |
| 65 | + |
| 66 | +Use the predicate overload when you need more control. |
| 67 | + |
| 68 | +```csharp |
| 69 | +using S1API.Entities; |
| 70 | + |
| 71 | +var injection = new DialogueInjection( |
| 72 | + appliesToNpc: npc => npc.ID == "Philip" && npc.FullName.Contains("Philip"), |
| 73 | + container: "CasinoDialogue", |
| 74 | + from: "ROOT_NODE_GUID", |
| 75 | + to: "BANKING_NODE_GUID", |
| 76 | + label: "ASK_ABOUT_PAYOUTS", |
| 77 | + text: "Quick question about payouts.", |
| 78 | + onConfirmed: OnAskAboutPayouts); |
| 79 | +``` |
| 80 | + |
| 81 | +This is helpful when multiple NPC variants share related data or when your targeting rule depends on runtime state. |
| 82 | + |
| 83 | +## Field Reference |
| 84 | + |
| 85 | +Each `DialogueInjection` instance needs the following values: |
| 86 | + |
| 87 | +- `AppliesTo`: Predicate used to decide whether a world NPC should receive the injection |
| 88 | +- `ContainerName`: Name of the target dialogue container |
| 89 | +- `FromNodeGuid`: GUID of the node that should receive the new choice |
| 90 | +- `ToNodeGuid`: GUID of the node the new choice should lead to |
| 91 | +- `ChoiceLabel`: Internal identifier used for callback registration |
| 92 | +- `ChoiceText`: Text shown to the player in the dialogue UI |
| 93 | +- `OnConfirmed`: Action invoked when the player selects the injected choice |
| 94 | + |
| 95 | +## How Registration Works |
| 96 | + |
| 97 | +When you call `DialogueInjector.Register(...)`: |
| 98 | + |
| 99 | +1. The injection is queued |
| 100 | +2. S1API starts a lightweight wait loop if one is not already running |
| 101 | +3. The loop checks loaded NPCs until one matches `AppliesTo` |
| 102 | +4. The injector resolves the requested container by name |
| 103 | +5. The injector finds the source node by `FromNodeGuid` |
| 104 | +6. A new choice and node link are added to that dialogue container |
| 105 | +7. `DialogueChoiceListener` binds your `OnConfirmed` callback to `ChoiceLabel` |
| 106 | + |
| 107 | +This means you can usually register injections during your mod setup without waiting for the NPC to already be spawned. |
| 108 | + |
| 109 | +## Example |
| 110 | + |
| 111 | +The example below adds a new question to an existing NPC dialogue and sends the player into a custom follow-up node when selected. |
| 112 | + |
| 113 | +```csharp |
| 114 | +using S1API.Dialogues; |
| 115 | + |
| 116 | +public static class PayoutDialogueFeature |
| 117 | +{ |
| 118 | + public static void Register() |
| 119 | + { |
| 120 | + DialogueInjector.Register(new DialogueInjection( |
| 121 | + npc: "Philip", |
| 122 | + container: "CasinoDialogue", |
| 123 | + from: "INTRO_NODE_GUID", |
| 124 | + to: "PAYOUT_INFO_NODE_GUID", |
| 125 | + label: "ASK_PAYOUT_QUESTION", |
| 126 | + text: "Quick question about payouts.", |
| 127 | + onConfirmed: OnPayoutQuestionSelected)); |
| 128 | + } |
| 129 | + |
| 130 | + private static void OnPayoutQuestionSelected() |
| 131 | + { |
| 132 | + // Put your feature logic here |
| 133 | + } |
| 134 | +} |
| 135 | +``` |
| 136 | + |
| 137 | +In practice, the target node identified by `ToNodeGuid` must already exist in the container you are linking into. |
| 138 | + |
| 139 | +## Requirements and Limitations |
| 140 | + |
| 141 | +- `ContainerName` must match a real container name exactly |
| 142 | +- `FromNodeGuid` must point to an existing node in that container |
| 143 | +- `ToNodeGuid` must point to an existing destination node |
| 144 | +- `ChoiceLabel` should be unique enough for your mod's callback usage |
| 145 | +- If the NPC, container, or node cannot be found, the injection is skipped silently |
| 146 | + |
| 147 | +Because this API works against existing dialogue assets, you should verify container names and GUIDs carefully before shipping. |
| 148 | + |
| 149 | +## Best Practices |
| 150 | + |
| 151 | +- Prefer stable NPC IDs over loose matching when possible |
| 152 | +- Keep `ChoiceLabel` descriptive and mod-specific to avoid collisions |
| 153 | +- Use player-facing `ChoiceText` that fits the NPC's existing conversation tone |
| 154 | +- Keep `OnConfirmed` fast and push larger flows into your own dialogue nodes or systems |
| 155 | +- Test the target dialogue path in-game after save loads and scene changes |
| 156 | + |
| 157 | +## Troubleshooting |
| 158 | + |
| 159 | +### The choice never appears |
| 160 | + |
| 161 | +Check these first: |
| 162 | + |
| 163 | +- The NPC ID or predicate actually matches a loaded `S1API.Entities.NPC` |
| 164 | +- The container name is correct |
| 165 | +- The source node GUID exists in that container |
| 166 | +- The target node GUID exists and is reachable |
| 167 | + |
| 168 | +### The callback does not run |
| 169 | + |
| 170 | +Make sure: |
| 171 | + |
| 172 | +- `ChoiceLabel` is unique for the injected choice |
| 173 | +- `OnConfirmed` does not throw immediately |
| 174 | +- Another mod is not reusing the same choice label in the same dialogue flow |
| 175 | + |
| 176 | +## Next Steps |
| 177 | + |
| 178 | +- Read **[Dialogue System](dialogue-system.md)** for building full containers and custom nodes |
| 179 | +- Use `DialogueInjection` for small extensions and `Dialogue.BuildAndRegisterContainer(...)` for larger custom flows |
0 commit comments