Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
227 changes: 227 additions & 0 deletions docs/NPCConversation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
# NPC Conversation

`NPC::Conversation` is the central runtime state holder for NPC dialog in OpenKore.

The old `%talk` and `$ai_v{'npc_talk'}` globals still exist only as compatibility mirrors inside this module. Production code should not read or write them directly anymore.

## State Model

The conversation module tracks one normalized conversation snapshot with:

- whether a conversation is active
- the current prompt state
- packed `npc_id` and numeric `name_id`
- NPC name
- full dialog text and per-line text
- current response list
- image name, if present
- last update time
- sequence id
- local waiting-for-server flag
- last error string

Primary prompt states:

- `CLOSED`
- `OPENING`
- `TEXT`
- `NEXT`
- `RESPONSES`
- `NUMBER_INPUT`
- `TEXT_INPUT`
- `WAITING_SERVER`
- `CLOSING`
- `BUY_OR_SELL`
- `STORE`
- `SELL`
- `CASH`
- `ERROR`

`current_state()` returns `WAITING_SERVER` when the local state is waiting for the next server packet. `prompt_state()` returns the raw prompt state without that overlay.

## Public API

Read helpers:

- `NPC::Conversation::is_open()`
- `NPC::Conversation::is_closed()`
- `NPC::Conversation::current_state()`
- `NPC::Conversation::prompt_state()`
- `NPC::Conversation::current_npc_id()`
- `NPC::Conversation::current_name_id()`
- `NPC::Conversation::current_npc_name()`
- `NPC::Conversation::text()`
- `NPC::Conversation::text_lines()`
- `NPC::Conversation::last_text()`
- `NPC::Conversation::responses()`
- `NPC::Conversation::response_count()`
- `NPC::Conversation::response_at($index)`
- `NPC::Conversation::find_response($text)`
- `NPC::Conversation::find_response_regex($pattern, $flags)`
- `NPC::Conversation::expects_continue()`
- `NPC::Conversation::expects_response()`
- `NPC::Conversation::expects_number()`
- `NPC::Conversation::expects_text()`
- `NPC::Conversation::can_close()`
- `NPC::Conversation::last_update_time()`
- `NPC::Conversation::sequence_id()`
- `NPC::Conversation::snapshot()`
- `NPC::Conversation::debug_string()`

Packet-side transitions:

- `NPC::Conversation::on_text_packet(...)`
- `NPC::Conversation::on_continue_packet(...)`
- `NPC::Conversation::on_responses_packet(...)`
- `NPC::Conversation::on_number_input_packet(...)`
- `NPC::Conversation::on_text_input_packet(...)`
- `NPC::Conversation::on_close_packet(...)`
- `NPC::Conversation::on_clear_packet(...)`
- `NPC::Conversation::on_shop_begin(...)`
- `NPC::Conversation::on_store_list(...)`
- `NPC::Conversation::on_sell_list(...)`
- `NPC::Conversation::on_cash_dealer(...)`
- `NPC::Conversation::on_image_packet(...)`
- `NPC::Conversation::clear_image()`
- `NPC::Conversation::on_error($message)`

Send-side helpers:

- `NPC::Conversation::start($npc_id, %info)`
- `NPC::Conversation::continue()`
- `NPC::Conversation::select_response($index)`
- `NPC::Conversation::select_response_text($text)`
- `NPC::Conversation::select_response_regex($pattern, $flags)`
- `NPC::Conversation::send_number($number)`
- `NPC::Conversation::send_text($text)`
- `NPC::Conversation::choose_buy_or_sell('buy'|'sell')`
- `NPC::Conversation::close()`
- `NPC::Conversation::cancel()`
- `NPC::Conversation::reset(%opts)`

## Packet Handler Usage

Receive handlers should stay thin. Parse packet payloads, then hand the state transition to `NPC::Conversation`.

Example:

```perl
NPC::Conversation::on_responses_packet(
npc_id => $ID,
name_id => $nameID,
npc_name => getNPCName($ID),
responses => \@responses,
);
```

Do not append to `%talk`, set `$ai_v{'npc_talk'}`, or interpret prompt state outside the module.

## Command Usage

Console commands and task logic should interact through the module:

```perl
NPC::Conversation::continue();
NPC::Conversation::select_response($index);
NPC::Conversation::send_number($number);
NPC::Conversation::send_text($text);
NPC::Conversation::cancel();
NPC::Conversation::close();
```

New direct commands:

- `npcTalkContinue`
- `npcTalkSelect <index|text>`
- `npcTalkSelectRegex </regex/[flags]|pattern>`
- `npcTalkNumber <number>`
- `npcTalkText <text>`
- `npcTalkClose`
- `npcTalkCancel`
- `npcTalkReset`
- `npcTalkDebug`

## eventMacro Usage

State conditions now available:

- `npcTalkActive`
- `npcTalkState`
- `npcTalkText`
- `npcTalkTextRegex`
- `npcTalkHasResponses`
- `npcTalkResponseCount`
- `npcTalkResponse`
- `npcTalkResponseRegex`
- `npcTalkExpectsContinue`
- `npcTalkExpectsResponse`
- `npcTalkExpectsNumber`
- `npcTalkExpectsText`
- `npcTalkNpcName`
- `npcTalkNpcId`

Example:

```text
automacro npc_can_continue {
npcTalkExpectsContinue 1
call {
npcTalkContinue
}
}
```

```text
automacro npc_choose_city {
npcTalkActive 1
npcTalkTextRegex /Choose your destination/
npcTalkResponse "Prontera"
call {
npcTalkSelect Prontera
}
}
```

Existing `NpcMsg*` conditions remain available and still work from the legacy `npc_talk` hook flow.

## Hooks

New hooks emitted by the module:

- `npc_talk_opened`
- `npc_talk_text`
- `npc_talk_responses`
- `npc_talk_number_input`
- `npc_talk_text_input`
- `npc_talk_state_changed`
- `npc_talk_closed`
- `npc_talk_error`

The legacy `npc_talk` and `npc_talk_done` hooks are still emitted by receive handling for compatibility.

## Debugging

Use `npcTalkDebug` to print a sanitized snapshot of the current conversation state.

Typical output includes:

- active flag
- prompt state and current state
- NPC id and name
- text lines
- response list
- waiting state
- sequence id
- last error

## Migration Notes

For plugin authors:

- stop reading `%talk` directly
- stop reading `$ai_v{'npc_talk'}` directly
- call `NPC::Conversation` for state reads
- call `NPC::Conversation` for response sends
- prefer new `npc_talk_state_changed` style hooks if you need stateful behavior

The compatibility mirror is temporary and should be treated as deprecated.
96 changes: 96 additions & 0 deletions docs/rathena-npc-conversation-test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// rAthena NPC conversation coverage script for OpenKore NPC::Conversation testing.
// Add to a test NPC map and talk to the NPCs below from OpenKore.

prontera,150,180,4 script OK_NPC_MES_CLOSE 4_F_KAFRA1,{
mes "[MES_CLOSE]";
mes "Simple message then close.";
close;
}

prontera,152,180,4 script OK_NPC_NEXT 4_F_KAFRA1,{
mes "[NEXT]";
mes "First page.";
next;
mes "Second page.";
close;
}

prontera,154,180,4 script OK_NPC_SELECT 4_F_KAFRA1,{
mes "[SELECT]";
switch(select("Prontera","Payon","Alberta")) {
case 1:
mes "Prontera selected.";
break;
case 2:
mes "Payon selected.";
break;
case 3:
mes "Alberta selected.";
break;
}
close;
}

prontera,156,180,4 script OK_NPC_MENU_REPEAT 4_F_KAFRA1,{
mes "[MENU_REPEAT]";
switch(select("Same Label","Same Label","Different Label")) {
case 1:
mes "First repeated label.";
break;
case 2:
mes "Second repeated label.";
break;
case 3:
mes "Different label.";
break;
}
close;
}

prontera,158,180,4 script OK_NPC_NUMBER 4_F_KAFRA1,{
mes "[NUMBER]";
input .@value;
mes "You entered "+.@value+".";
close;
}

prontera,160,180,4 script OK_NPC_TEXT 4_F_KAFRA1,{
mes "[TEXT]";
input .@name$;
mes "Hello "+.@name$+".";
close;
}

prontera,162,180,4 script OK_NPC_COLOR 4_F_KAFRA1,{
mes "[COLOR]";
mes "^FF0000Red text^000000 and punctuation: ! ? , .";
next;
mes "Second line after colored text.";
close;
}

prontera,164,180,4 script OK_NPC_UNEXPECTED_CLOSE 4_F_KAFRA1,{
mes "[UNEXPECTED_CLOSE]";
mes "This closes abruptly.";
close2;
dispbottom "Server-side close2 reached.";
end;
}

prontera,166,180,4 script OK_NPC_SHOP 4_F_KAFRA1,{
callshop "OK_NPC_TESTSHOP",1;
end;
}

- shop OK_NPC_TESTSHOP -1,501:2,502:5,503:10

// Suggested manual checks:
// 1. mes + close
// 2. next flow
// 3. menu/select with unique labels
// 4. menu/select with repeated labels
// 5. numeric input
// 6. text input
// 7. color codes and punctuation
// 8. close2 / unexpected close
// 9. shop open path
21 changes: 21 additions & 0 deletions plugins/eventMacro/eventMacro/Condition/Base/NpcTalkState.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package eventMacro::Condition::Base::NpcTalkState;

use strict;

sub _hooks {
return ['in_game', 'npc_talk_state_changed'];
}

sub parse_wanted_state {
my ($self, $condition_code) = @_;

if (defined $condition_code && $condition_code =~ /^(0|1)$/) {
$self->{wanted_state} = $1;
return 1;
}

$self->{error} = "Value '$condition_code' Should be '0' or '1'";
return 0;
}

1;
24 changes: 24 additions & 0 deletions plugins/eventMacro/eventMacro/Condition/npcTalkActive.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package eventMacro::Condition::npcTalkActive;

use strict;

use base 'eventMacro::Condition';

use NPC::Conversation;
use eventMacro::Condition::Base::NpcTalkState;

sub _hooks {
eventMacro::Condition::Base::NpcTalkState::_hooks();
}

sub _parse_syntax {
my ($self, $condition_code) = @_;
eventMacro::Condition::Base::NpcTalkState::parse_wanted_state($self, $condition_code);
}

sub validate_condition {
my ($self, $callback_type, $callback_name, $args) = @_;
return $self->SUPER::validate_condition((NPC::Conversation::is_open() ? 1 : 0) == $self->{wanted_state} ? 1 : 0);
}

1;
24 changes: 24 additions & 0 deletions plugins/eventMacro/eventMacro/Condition/npcTalkExpectsContinue.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package eventMacro::Condition::npcTalkExpectsContinue;

use strict;

use base 'eventMacro::Condition';

use NPC::Conversation;
use eventMacro::Condition::Base::NpcTalkState;

sub _hooks {
eventMacro::Condition::Base::NpcTalkState::_hooks();
}

sub _parse_syntax {
my ($self, $condition_code) = @_;
eventMacro::Condition::Base::NpcTalkState::parse_wanted_state($self, $condition_code);
}

sub validate_condition {
my ($self, $callback_type, $callback_name, $args) = @_;
return $self->SUPER::validate_condition((NPC::Conversation::expects_continue() ? 1 : 0) == $self->{wanted_state} ? 1 : 0);
}

1;
Loading