From a35a8c2d1fe333b072f224e8eeb582011824eeea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Bosc=C3=A1?= Date: Wed, 1 Apr 2026 17:52:22 -0300 Subject: [PATCH 1/5] Sort quest missions and map hunt updates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Improve quest mission ordering and robustly handle server hunt updates. Commands.pm: sort mission entries by mission_index (fallback to 9999) then by numeric mobID so missions display in intended order. Network/Receive.pm: ignore mission updates for unknown quests early, and add a fallback mapping for servers that send updates keyed only by hunt_id — derive a mission_index from hunt_id and match against existing missions (allowing off-by-one), with safety checks to avoid applying updates to non-existent missions. These changes prevent incorrect ordering and missed/incorrect mission updates from varied server implementations. --- src/Commands.pm | 5 +++-- src/Network/Receive.pm | 18 +++++++++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/Commands.pm b/src/Commands.pm index d607e74c17..165172ef02 100644 --- a/src/Commands.pm +++ b/src/Commands.pm @@ -7091,7 +7091,9 @@ sub cmdQuest { my $quest = $questList->{$questID}; $msg .= swrite(sprintf("\@%s \@%s \@%s \@%s \@%s", ('>'x2), ('<'x5), ('<'x30), ('<'x10), ('<'x24)), [$k, $questID, $quests_lut{$questID} ? $quests_lut{$questID}{title} : '', $quest->{active} ? T("active") : T("inactive"), $quest->{time_expire} ? scalar localtime $quest->{time_expire} : '']); - foreach my $mobID (keys %{$quest->{missions}}) { + foreach my $mobID (sort { + ($quest->{missions}{$a}{mission_index} // 9999) <=> ($quest->{missions}{$b}{mission_index} // 9999) || $a <=> $b + } keys %{$quest->{missions}}) { my $mission = $quest->{missions}->{$mobID}; $msg .= swrite(sprintf("\@%s \@%s \@%s", ('>'x2), ('<'x30), ('<'x30)), [" -", $mission->{mob_name}, sprintf(defined $mission->{mob_goal} ? '%d/%d' : '%d', @{$mission}{qw(mob_count mob_goal)})]); @@ -8786,4 +8788,3 @@ sub cmdEden { 1; - diff --git a/src/Network/Receive.pm b/src/Network/Receive.pm index 0382a8cf37..8a167cc457 100644 --- a/src/Network/Receive.pm +++ b/src/Network/Receive.pm @@ -4891,6 +4891,8 @@ sub quest_update_mission_hunt { @{$mission}{@{$quest_info->{mission_keys}}} = unpack($quest_info->{mission_pack}, substr($args->{message}, $offset, $quest_info->{mission_len})); + next unless exists $questList->{$mission->{questID}}; + my $quest = \%{$questList->{$mission->{questID}}}; my $mission_id; @@ -4924,6 +4926,21 @@ sub quest_update_mission_hunt { } } + # Some servers can return mission updates keyed only by hunt identification. + # If direct lookup fails, map update by mission index from hunt_id. + if (!defined $mission_id && exists $mission->{hunt_id}) { + my $mission_index = $mission->{hunt_id} - ($mission->{questID} * 1000); + + foreach my $current_key (keys %{$quest->{missions}}) { + next unless exists $quest->{missions}->{$current_key}{mission_index}; + next unless $quest->{missions}->{$current_key}{mission_index} == $mission_index || $quest->{missions}->{$current_key}{mission_index} == $mission_index - 1; + $mission_id = $current_key; + last; + } + } + + next unless defined $mission_id && exists $quest->{missions}->{$mission_id}; + my $quest_mission = \%{$quest->{missions}->{$mission_id}}; $quest_mission->{mob_count} = $mission->{mob_count}; @@ -12604,4 +12621,3 @@ sub notify_accessible_mapname { } 1; - From 546b3ccff8173ca2c568578012bda15ba25395bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Bosc=C3=A1?= Date: Fri, 3 Apr 2026 00:25:28 -0300 Subject: [PATCH 2/5] Improve hunt mission mapping using recent kills Track the last killed monster (nameID and timestamp) and use this as a heuristic to resolve ambiguous quest hunt updates. Extract raw mission data earlier and advance the offset immediately after reading. Treat servers' hunt_id field as a usable hunt identifier only when it looks like a unique hunt token (greater than questID*1000), and prefer exact mission_index matches while retaining a legacy fallback (mission_index - 1). Add debug logging for ambiguous mappings and simplify hunt/mob lookup logic to better handle servers that omit mob_id in hunt updates. --- src/Network/Receive.pm | 65 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 53 insertions(+), 12 deletions(-) diff --git a/src/Network/Receive.pm b/src/Network/Receive.pm index 8a167cc457..2a909898b1 100644 --- a/src/Network/Receive.pm +++ b/src/Network/Receive.pm @@ -2435,6 +2435,8 @@ sub actor_died_or_disappeared { } elsif ($args->{type} == 1) { debug "Monster Died: " . $monster->name . " ($monster->{binID})\n", "parseMsg_damage"; $monster->{dead} = 1; + $self->{_last_killed_monster_nameID} = $monster->{nameID}; + $self->{_last_killed_monster_time} = time; if ((AI::action() ne "attack" || AI::args(0)->{ID} eq $ID) && ($config{itemsTakeAuto_party} && @@ -4889,17 +4891,45 @@ sub quest_update_mission_hunt { for (my $i = 0; $i < $args->{mission_amount}; $i++) { my $mission; - @{$mission}{@{$quest_info->{mission_keys}}} = unpack($quest_info->{mission_pack}, substr($args->{message}, $offset, $quest_info->{mission_len})); + my $raw_mission = substr($args->{message}, $offset, $quest_info->{mission_len}); + $offset += $quest_info->{mission_len}; + + @{$mission}{@{$quest_info->{mission_keys}}} = unpack($quest_info->{mission_pack}, $raw_mission); next unless exists $questList->{$mission->{questID}}; my $quest = \%{$questList->{$mission->{questID}}}; my $mission_id; + my $update_without_mob_id = exists $mission->{hunt_id} && !exists $mission->{mob_id}; + my $hunt_identifier = undef; + if (exists $mission->{hunt_id}) { + # Some servers send questID in this field instead of a unique hunt identifier. + # Only treat it as a usable hunt identifier when it looks like questID * 1000 + mission_id. + $hunt_identifier = $mission->{hunt_id} if $mission->{hunt_id} > ($mission->{questID} * 1000); + } + my $recent_kill_mission_id; + + # For hunt-only updates, if we just killed a monster, that is the strongest + # deterministic signal to map the mission. + if ($update_without_mob_id + && defined $self->{_last_killed_monster_nameID} + && defined $self->{_last_killed_monster_time} + && time - $self->{_last_killed_monster_time} <= 3) { + my @recent_kill_candidates = grep { + exists $quest->{missions}->{$_}{mob_id} + && $quest->{missions}->{$_}{mob_id} == $self->{_last_killed_monster_nameID} + } keys %{$quest->{missions}}; + $recent_kill_mission_id = $recent_kill_candidates[0] if @recent_kill_candidates == 1; + } + + if (defined $recent_kill_mission_id) { + $mission_id = $recent_kill_mission_id; + } # Mission is saved as hunt_id and server sent hunt_id - if (exists $mission->{hunt_id} && exists $quest->{missions}->{$mission->{hunt_id}}) { - $mission_id = $mission->{hunt_id}; + if (defined $hunt_identifier && exists $quest->{missions}->{$hunt_identifier}) { + $mission_id = $hunt_identifier; # Mission is saved as mob_id and server sent mob_id } elsif (exists $mission->{mob_id} && exists $quest->{missions}->{$mission->{mob_id}}) { @@ -4916,10 +4946,10 @@ sub quest_update_mission_hunt { } # Mission is saved as mob_id and server sent hunt_id - } elsif (exists $mission->{hunt_id} && !exists $quest->{missions}->{$mission->{hunt_id}}) { + } elsif (defined $hunt_identifier && !exists $quest->{missions}->{$hunt_identifier}) { # Search in the quest of a mission with this hunt_id foreach my $current_key (keys %{$quest->{missions}}) { - if (exists $quest->{missions}->{$current_key}{hunt_id} && $quest->{missions}->{$current_key}{hunt_id} == $mission->{hunt_id}) { + if (exists $quest->{missions}->{$current_key}{hunt_id} && $quest->{missions}->{$current_key}{hunt_id} == $hunt_identifier) { $mission_id = $quest->{missions}->{$current_key}{mob_id}; last; } @@ -4928,14 +4958,27 @@ sub quest_update_mission_hunt { # Some servers can return mission updates keyed only by hunt identification. # If direct lookup fails, map update by mission index from hunt_id. - if (!defined $mission_id && exists $mission->{hunt_id}) { - my $mission_index = $mission->{hunt_id} - ($mission->{questID} * 1000); + if (!defined $mission_id && defined $hunt_identifier) { + my $mission_index = $hunt_identifier - ($mission->{questID} * 1000); + my @exact_candidates; + my @legacy_candidates; foreach my $current_key (keys %{$quest->{missions}}) { next unless exists $quest->{missions}->{$current_key}{mission_index}; - next unless $quest->{missions}->{$current_key}{mission_index} == $mission_index || $quest->{missions}->{$current_key}{mission_index} == $mission_index - 1; - $mission_id = $current_key; - last; + my $current_index = $quest->{missions}->{$current_key}{mission_index}; + push @exact_candidates, $current_key if $current_index == $mission_index; + push @legacy_candidates, $current_key if $current_index == $mission_index - 1; + } + + # Prefer exact mission_index match first. + if (@exact_candidates == 1) { + $mission_id = $exact_candidates[0]; + } elsif (!@exact_candidates && @legacy_candidates == 1) { + # Compatibility fallback for servers that report mission_index starting at 1. + $mission_id = $legacy_candidates[0]; + } elsif (@exact_candidates > 1 || @legacy_candidates > 1) { + debug TF("Quest mission update ignored due to ambiguous hunt mapping (quest: %d, hunt_id: %d, mission_index: %d)\n", + $mission->{questID}, $mission->{hunt_id}, $mission_index), "info"; } } @@ -4956,8 +4999,6 @@ sub quest_update_mission_hunt { } } - $offset += $quest_info->{mission_len}; - Plugins::callHook('quest_mission_updated', { questID => $quest_mission->{questID}, mission_id => $mission_id, From fe7894a6ad6538589b146de7ddd1402b047ad429 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Bosc=C3=A1?= Date: Fri, 3 Apr 2026 01:57:48 -0300 Subject: [PATCH 3/5] Improve quest hunt mapping and kill tracking Only record a monster as "last killed" if the player or party dealt damage, avoiding misattribution from non-player kills. Track per-quest packet sequence for multi-entry quest updates and simplify mission_index matching logic to pick the proper mission_id deterministically. Add fallbacks: prefer the current attack target for hunt-only updates, then recent kills for single-entry packets; use packet order within a quest when servers omit mob IDs. Reduce noisy quest progress logs by only showing updates for single-packet updates or when mission progress actually changes, and prefer a combat-context mob name in those messages. --- src/Network/Receive.pm | 110 ++++++++++++++++++++++++++--------------- 1 file changed, 71 insertions(+), 39 deletions(-) diff --git a/src/Network/Receive.pm b/src/Network/Receive.pm index 2a909898b1..61d442abaf 100644 --- a/src/Network/Receive.pm +++ b/src/Network/Receive.pm @@ -2435,8 +2435,10 @@ sub actor_died_or_disappeared { } elsif ($args->{type} == 1) { debug "Monster Died: " . $monster->name . " ($monster->{binID})\n", "parseMsg_damage"; $monster->{dead} = 1; - $self->{_last_killed_monster_nameID} = $monster->{nameID}; - $self->{_last_killed_monster_time} = time; + if (($monster->{dmgFromYou} || 0) > 0 || ($monster->{dmgFromParty} || 0) > 0) { + $self->{_last_killed_monster_nameID} = $monster->{nameID}; + $self->{_last_killed_monster_time} = time; + } if ((AI::action() ne "attack" || AI::args(0)->{ID} eq $ID) && ($config{itemsTakeAuto_party} && @@ -4888,6 +4890,8 @@ sub quest_update_mission_hunt { $args->{mission_amount} = (length $args->{message}) / ($quest_info->{mission_len}); } + my %quest_update_seq; + for (my $i = 0; $i < $args->{mission_amount}; $i++) { my $mission; @@ -4899,6 +4903,8 @@ sub quest_update_mission_hunt { next unless exists $questList->{$mission->{questID}}; my $quest = \%{$questList->{$mission->{questID}}}; + my $quest_packet_index = $quest_update_seq{$mission->{questID}} // 0; + $quest_update_seq{$mission->{questID}} = $quest_packet_index + 1; my $mission_id; my $update_without_mob_id = exists $mission->{hunt_id} && !exists $mission->{mob_id}; @@ -4908,25 +4914,6 @@ sub quest_update_mission_hunt { # Only treat it as a usable hunt identifier when it looks like questID * 1000 + mission_id. $hunt_identifier = $mission->{hunt_id} if $mission->{hunt_id} > ($mission->{questID} * 1000); } - my $recent_kill_mission_id; - - # For hunt-only updates, if we just killed a monster, that is the strongest - # deterministic signal to map the mission. - if ($update_without_mob_id - && defined $self->{_last_killed_monster_nameID} - && defined $self->{_last_killed_monster_time} - && time - $self->{_last_killed_monster_time} <= 3) { - my @recent_kill_candidates = grep { - exists $quest->{missions}->{$_}{mob_id} - && $quest->{missions}->{$_}{mob_id} == $self->{_last_killed_monster_nameID} - } keys %{$quest->{missions}}; - $recent_kill_mission_id = $recent_kill_candidates[0] if @recent_kill_candidates == 1; - } - - if (defined $recent_kill_mission_id) { - $mission_id = $recent_kill_mission_id; - } - # Mission is saved as hunt_id and server sent hunt_id if (defined $hunt_identifier && exists $quest->{missions}->{$hunt_identifier}) { $mission_id = $hunt_identifier; @@ -4960,42 +4947,87 @@ sub quest_update_mission_hunt { # If direct lookup fails, map update by mission index from hunt_id. if (!defined $mission_id && defined $hunt_identifier) { my $mission_index = $hunt_identifier - ($mission->{questID} * 1000); - my @exact_candidates; - my @legacy_candidates; foreach my $current_key (keys %{$quest->{missions}}) { next unless exists $quest->{missions}->{$current_key}{mission_index}; - my $current_index = $quest->{missions}->{$current_key}{mission_index}; - push @exact_candidates, $current_key if $current_index == $mission_index; - push @legacy_candidates, $current_key if $current_index == $mission_index - 1; + next unless $quest->{missions}->{$current_key}{mission_index} == $mission_index + || $quest->{missions}->{$current_key}{mission_index} == $mission_index - 1; + $mission_id = $current_key; + last; + } + } + + # Some servers send hunt-only updates without a usable hunt_id for each mission. + # In multi-entry packets, fall back to packet order within each quest. + if (!defined $mission_id && $update_without_mob_id) { + my @index_candidates = grep { + exists $quest->{missions}->{$_}{mission_index} + && $quest->{missions}->{$_}{mission_index} == $quest_packet_index + } keys %{$quest->{missions}}; + $mission_id = $index_candidates[0] if @index_candidates == 1; + } + + # As a last resort for hunt-only updates, use the recent killed mob. + if (!defined $mission_id && $update_without_mob_id + && $args->{mission_amount} == 1 + ) { + my $candidate_mob_id; + + # Prefer current attack target mob when available. + if (AI::action() eq 'attack' && AI::args(0) && AI::args(0)->{ID}) { + my $target = $monstersList->getByID(AI::args(0)->{ID}); + $candidate_mob_id = $target->{nameID} if $target; } - # Prefer exact mission_index match first. - if (@exact_candidates == 1) { - $mission_id = $exact_candidates[0]; - } elsif (!@exact_candidates && @legacy_candidates == 1) { - # Compatibility fallback for servers that report mission_index starting at 1. - $mission_id = $legacy_candidates[0]; - } elsif (@exact_candidates > 1 || @legacy_candidates > 1) { - debug TF("Quest mission update ignored due to ambiguous hunt mapping (quest: %d, hunt_id: %d, mission_index: %d)\n", - $mission->{questID}, $mission->{hunt_id}, $mission_index), "info"; + # Fallback to the last killed mob when current target is unavailable. + $candidate_mob_id = $self->{_last_killed_monster_nameID} + if !defined $candidate_mob_id + && defined $self->{_last_killed_monster_nameID} + && defined $self->{_last_killed_monster_time}; + + if (defined $candidate_mob_id) { + my @recent_kill_candidates = grep { + exists $quest->{missions}->{$_}{mob_id} + && $quest->{missions}->{$_}{mob_id} == $candidate_mob_id + } keys %{$quest->{missions}}; + $mission_id = $recent_kill_candidates[0] if @recent_kill_candidates == 1; } } next unless defined $mission_id && exists $quest->{missions}->{$mission_id}; my $quest_mission = \%{$quest->{missions}->{$mission_id}}; + my $old_count = $quest_mission->{mob_count}; + my $old_goal = $quest_mission->{mob_goal}; $quest_mission->{mob_count} = $mission->{mob_count}; $quest_mission->{mob_goal} = $mission->{mob_goal}; - + my $progress_mob_name = $quest_mission->{mob_name}; + + # For hunt-only packets, prefer a combat-context name in progress logs. + if (!exists $mission->{mob_id} + && AI::action() eq 'attack' && AI::args(0) && AI::args(0)->{ID}) { + my $target = $monstersList->getByID(AI::args(0)->{ID}); + $progress_mob_name = $target->name if $target; + } elsif (!exists $mission->{mob_id} + && defined $self->{_last_killed_monster_nameID}) { + my @last_kill_candidates = grep { + exists $quest->{missions}->{$_}{mob_id} + && $quest->{missions}->{$_}{mob_id} == $self->{_last_killed_monster_nameID} + } keys %{$quest->{missions}}; + if (@last_kill_candidates == 1 && defined $quest->{missions}{$last_kill_candidates[0]}{mob_name}) { + $progress_mob_name = $quest->{missions}{$last_kill_candidates[0]}{mob_name}; + } + } + my $mission_changed = !defined $old_count || $old_count != $quest_mission->{mob_count} + || !defined $old_goal || $old_goal != $quest_mission->{mob_goal}; debug "- MobID: $mission->{mob_id} - Name: $mission->{mob_name} - Count: $mission->{mob_count} - Goal: $mission->{mob_goal}\n", "info"; - if ($config{questDisplayStyle}) { + if ($config{questDisplayStyle} && ($args->{mission_amount} == 1 || $mission_changed)) { if ($config{questDisplayStyle} >= 2) { - warning TF("[%s] Quest - defeated [%s] progress (%s/%s)\n", $quests_lut{$mission->{questID}} ? "$quests_lut{$mission->{questID}}{title} ($mission->{questID})" : $mission->{questID}, $quest_mission->{mob_name}, $quest_mission->{mob_count}, $quest_mission->{mob_goal}), "info"; + warning TF("[%s] Quest - defeated [%s] progress (%s/%s)\n", $quests_lut{$mission->{questID}} ? "$quests_lut{$mission->{questID}}{title} ($mission->{questID})" : $mission->{questID}, $progress_mob_name, $quest_mission->{mob_count}, $quest_mission->{mob_goal}), "info"; } else { - warning TF("%s [%s/%s]\n", $quest_mission->{mob_name}, $quest_mission->{mob_count}, $quest_mission->{mob_goal}), "info"; + warning TF("%s [%s/%s]\n", $progress_mob_name, $quest_mission->{mob_count}, $quest_mission->{mob_goal}), "info"; } } From 5382e4deee4ce8f9fa1cae69338a7a42597ce934 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Bosc=C3=A1?= Date: Mon, 6 Apr 2026 19:52:30 -0300 Subject: [PATCH 4/5] Prefer mission hunt_id; drop last-killed fallback Refactor quest hunt update logic to prefer direct identifiers and remove reliance on a tracked "last killed" mob. Stop storing _last_killed_monster_nameID/_time when actors die. In quest_update_mission_hunt, use mission->{hunt_id} for direct lookups, add safer fallbacks that map hunt-only updates by the current attack target or by matching mission progress for single-entry updates, and remove legacy heuristics that used the last-killed mob for mapping or name selection. Progress logging now always uses quest_mission->{mob_name}. These changes improve robustness against servers sending hunt-only updates and avoid stale/incorrect last-kill metadata. --- src/Network/Receive.pm | 78 +++++++++++++++--------------------------- 1 file changed, 27 insertions(+), 51 deletions(-) diff --git a/src/Network/Receive.pm b/src/Network/Receive.pm index 61d442abaf..57a9201bf0 100644 --- a/src/Network/Receive.pm +++ b/src/Network/Receive.pm @@ -2435,10 +2435,6 @@ sub actor_died_or_disappeared { } elsif ($args->{type} == 1) { debug "Monster Died: " . $monster->name . " ($monster->{binID})\n", "parseMsg_damage"; $monster->{dead} = 1; - if (($monster->{dmgFromYou} || 0) > 0 || ($monster->{dmgFromParty} || 0) > 0) { - $self->{_last_killed_monster_nameID} = $monster->{nameID}; - $self->{_last_killed_monster_time} = time; - } if ((AI::action() ne "attack" || AI::args(0)->{ID} eq $ID) && ($config{itemsTakeAuto_party} && @@ -4914,9 +4910,12 @@ sub quest_update_mission_hunt { # Only treat it as a usable hunt identifier when it looks like questID * 1000 + mission_id. $hunt_identifier = $mission->{hunt_id} if $mission->{hunt_id} > ($mission->{questID} * 1000); } + + # Primary path (default behavior): use direct identifiers from packet/state. + # Only if these fail do we apply fallback heuristics below. # Mission is saved as hunt_id and server sent hunt_id - if (defined $hunt_identifier && exists $quest->{missions}->{$hunt_identifier}) { - $mission_id = $hunt_identifier; + if (exists $mission->{hunt_id} && exists $quest->{missions}->{$mission->{hunt_id}}) { + $mission_id = $mission->{hunt_id}; # Mission is saved as mob_id and server sent mob_id } elsif (exists $mission->{mob_id} && exists $quest->{missions}->{$mission->{mob_id}}) { @@ -4933,18 +4932,17 @@ sub quest_update_mission_hunt { } # Mission is saved as mob_id and server sent hunt_id - } elsif (defined $hunt_identifier && !exists $quest->{missions}->{$hunt_identifier}) { + } elsif (exists $mission->{hunt_id} && !exists $quest->{missions}->{$mission->{hunt_id}}) { # Search in the quest of a mission with this hunt_id foreach my $current_key (keys %{$quest->{missions}}) { - if (exists $quest->{missions}->{$current_key}{hunt_id} && $quest->{missions}->{$current_key}{hunt_id} == $hunt_identifier) { + if (exists $quest->{missions}->{$current_key}{hunt_id} && $quest->{missions}->{$current_key}{hunt_id} == $mission->{hunt_id}) { $mission_id = $quest->{missions}->{$current_key}{mob_id}; last; } } } - # Some servers can return mission updates keyed only by hunt identification. - # If direct lookup fails, map update by mission index from hunt_id. + # Fallback: when default lookup fails, use normalized hunt_id heuristics. if (!defined $mission_id && defined $hunt_identifier) { my $mission_index = $hunt_identifier - ($mission->{questID} * 1000); @@ -4967,33 +4965,28 @@ sub quest_update_mission_hunt { $mission_id = $index_candidates[0] if @index_candidates == 1; } - # As a last resort for hunt-only updates, use the recent killed mob. - if (!defined $mission_id && $update_without_mob_id - && $args->{mission_amount} == 1 - ) { - my $candidate_mob_id; - - # Prefer current attack target mob when available. - if (AI::action() eq 'attack' && AI::args(0) && AI::args(0)->{ID}) { - my $target = $monstersList->getByID(AI::args(0)->{ID}); - $candidate_mob_id = $target->{nameID} if $target; - } - - # Fallback to the last killed mob when current target is unavailable. - $candidate_mob_id = $self->{_last_killed_monster_nameID} - if !defined $candidate_mob_id - && defined $self->{_last_killed_monster_nameID} - && defined $self->{_last_killed_monster_time}; - - if (defined $candidate_mob_id) { - my @recent_kill_candidates = grep { + # Last-resort for single-entry hunt-only updates: map by current attack target mob_id. + if (!defined $mission_id && $update_without_mob_id && $args->{mission_amount} == 1 + && AI::action() eq 'attack' && AI::args(0) && AI::args(0)->{ID}) { + my $target = $monstersList->getByID(AI::args(0)->{ID}); + if ($target && defined $target->{nameID}) { + my @target_candidates = grep { exists $quest->{missions}->{$_}{mob_id} - && $quest->{missions}->{$_}{mob_id} == $candidate_mob_id + && $quest->{missions}->{$_}{mob_id} == $target->{nameID} } keys %{$quest->{missions}}; - $mission_id = $recent_kill_candidates[0] if @recent_kill_candidates == 1; + $mission_id = $target_candidates[0] if @target_candidates == 1; } } + # For single-entry hunt-only updates, align slot by current quest progress when possible. + if ($update_without_mob_id && $args->{mission_amount} == 1) { + my @progress_candidates = grep { + exists $quest->{missions}->{$_}{mob_count} + && $mission->{mob_count} == $quest->{missions}->{$_}{mob_count} + 1 + } keys %{$quest->{missions}}; + $mission_id = $progress_candidates[0] if @progress_candidates == 1; + } + next unless defined $mission_id && exists $quest->{missions}->{$mission_id}; my $quest_mission = \%{$quest->{missions}->{$mission_id}}; @@ -5002,32 +4995,15 @@ sub quest_update_mission_hunt { $quest_mission->{mob_count} = $mission->{mob_count}; $quest_mission->{mob_goal} = $mission->{mob_goal}; - my $progress_mob_name = $quest_mission->{mob_name}; - - # For hunt-only packets, prefer a combat-context name in progress logs. - if (!exists $mission->{mob_id} - && AI::action() eq 'attack' && AI::args(0) && AI::args(0)->{ID}) { - my $target = $monstersList->getByID(AI::args(0)->{ID}); - $progress_mob_name = $target->name if $target; - } elsif (!exists $mission->{mob_id} - && defined $self->{_last_killed_monster_nameID}) { - my @last_kill_candidates = grep { - exists $quest->{missions}->{$_}{mob_id} - && $quest->{missions}->{$_}{mob_id} == $self->{_last_killed_monster_nameID} - } keys %{$quest->{missions}}; - if (@last_kill_candidates == 1 && defined $quest->{missions}{$last_kill_candidates[0]}{mob_name}) { - $progress_mob_name = $quest->{missions}{$last_kill_candidates[0]}{mob_name}; - } - } my $mission_changed = !defined $old_count || $old_count != $quest_mission->{mob_count} || !defined $old_goal || $old_goal != $quest_mission->{mob_goal}; debug "- MobID: $mission->{mob_id} - Name: $mission->{mob_name} - Count: $mission->{mob_count} - Goal: $mission->{mob_goal}\n", "info"; if ($config{questDisplayStyle} && ($args->{mission_amount} == 1 || $mission_changed)) { if ($config{questDisplayStyle} >= 2) { - warning TF("[%s] Quest - defeated [%s] progress (%s/%s)\n", $quests_lut{$mission->{questID}} ? "$quests_lut{$mission->{questID}}{title} ($mission->{questID})" : $mission->{questID}, $progress_mob_name, $quest_mission->{mob_count}, $quest_mission->{mob_goal}), "info"; + warning TF("[%s] Quest - defeated [%s] progress (%s/%s)\n", $quests_lut{$mission->{questID}} ? "$quests_lut{$mission->{questID}}{title} ($mission->{questID})" : $mission->{questID}, $quest_mission->{mob_name}, $quest_mission->{mob_count}, $quest_mission->{mob_goal}), "info"; } else { - warning TF("%s [%s/%s]\n", $progress_mob_name, $quest_mission->{mob_count}, $quest_mission->{mob_goal}), "info"; + warning TF("%s [%s/%s]\n", $quest_mission->{mob_name}, $quest_mission->{mob_count}, $quest_mission->{mob_goal}), "info"; } } From 74b9aa0ad40aa0a1b33ae629db33451fbf5b7bd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Bosc=C3=A1?= Date: Wed, 8 Apr 2026 13:21:50 -0300 Subject: [PATCH 5/5] Improve quest hunt update mapping and kill tracking Track the last killed monster (nameID + timestamp) when actors die to help map hunt-only quest updates. Replace fragile packet-sequence and multiple heuristic fallbacks with a more deterministic mapping: prefer direct hunt/mob IDs, use a hunt_identifier, then match mission_index with exact and legacy (index-1) candidates, and log ambiguous mappings. Remove several legacy fallbacks (packet order, AI-target mapping, progress-based mapping) and the unused quest_update_seq state. Also simplify quest display triggering and tighten handling for server inconsistencies to make mission mapping more reliable (uses a 3s recent-kill window to disambiguate). --- src/Network/Receive.pm | 94 +++++++++++++++++++----------------------- 1 file changed, 42 insertions(+), 52 deletions(-) diff --git a/src/Network/Receive.pm b/src/Network/Receive.pm index c5f0248d20..2a909898b1 100644 --- a/src/Network/Receive.pm +++ b/src/Network/Receive.pm @@ -2435,6 +2435,8 @@ sub actor_died_or_disappeared { } elsif ($args->{type} == 1) { debug "Monster Died: " . $monster->name . " ($monster->{binID})\n", "parseMsg_damage"; $monster->{dead} = 1; + $self->{_last_killed_monster_nameID} = $monster->{nameID}; + $self->{_last_killed_monster_time} = time; if ((AI::action() ne "attack" || AI::args(0)->{ID} eq $ID) && ($config{itemsTakeAuto_party} && @@ -4886,8 +4888,6 @@ sub quest_update_mission_hunt { $args->{mission_amount} = (length $args->{message}) / ($quest_info->{mission_len}); } - my %quest_update_seq; - for (my $i = 0; $i < $args->{mission_amount}; $i++) { my $mission; @@ -4898,11 +4898,7 @@ sub quest_update_mission_hunt { next unless exists $questList->{$mission->{questID}}; - next unless exists $questList->{$mission->{questID}}; - my $quest = \%{$questList->{$mission->{questID}}}; - my $quest_packet_index = $quest_update_seq{$mission->{questID}} // 0; - $quest_update_seq{$mission->{questID}} = $quest_packet_index + 1; my $mission_id; my $update_without_mob_id = exists $mission->{hunt_id} && !exists $mission->{mob_id}; @@ -4912,12 +4908,28 @@ sub quest_update_mission_hunt { # Only treat it as a usable hunt identifier when it looks like questID * 1000 + mission_id. $hunt_identifier = $mission->{hunt_id} if $mission->{hunt_id} > ($mission->{questID} * 1000); } + my $recent_kill_mission_id; + + # For hunt-only updates, if we just killed a monster, that is the strongest + # deterministic signal to map the mission. + if ($update_without_mob_id + && defined $self->{_last_killed_monster_nameID} + && defined $self->{_last_killed_monster_time} + && time - $self->{_last_killed_monster_time} <= 3) { + my @recent_kill_candidates = grep { + exists $quest->{missions}->{$_}{mob_id} + && $quest->{missions}->{$_}{mob_id} == $self->{_last_killed_monster_nameID} + } keys %{$quest->{missions}}; + $recent_kill_mission_id = $recent_kill_candidates[0] if @recent_kill_candidates == 1; + } + + if (defined $recent_kill_mission_id) { + $mission_id = $recent_kill_mission_id; + } - # Primary path (default behavior): use direct identifiers from packet/state. - # Only if these fail do we apply fallback heuristics below. # Mission is saved as hunt_id and server sent hunt_id - if (exists $mission->{hunt_id} && exists $quest->{missions}->{$mission->{hunt_id}}) { - $mission_id = $mission->{hunt_id}; + if (defined $hunt_identifier && exists $quest->{missions}->{$hunt_identifier}) { + $mission_id = $hunt_identifier; # Mission is saved as mob_id and server sent mob_id } elsif (exists $mission->{mob_id} && exists $quest->{missions}->{$mission->{mob_id}}) { @@ -4934,74 +4946,52 @@ sub quest_update_mission_hunt { } # Mission is saved as mob_id and server sent hunt_id - } elsif (exists $mission->{hunt_id} && !exists $quest->{missions}->{$mission->{hunt_id}}) { + } elsif (defined $hunt_identifier && !exists $quest->{missions}->{$hunt_identifier}) { # Search in the quest of a mission with this hunt_id foreach my $current_key (keys %{$quest->{missions}}) { - if (exists $quest->{missions}->{$current_key}{hunt_id} && $quest->{missions}->{$current_key}{hunt_id} == $mission->{hunt_id}) { + if (exists $quest->{missions}->{$current_key}{hunt_id} && $quest->{missions}->{$current_key}{hunt_id} == $hunt_identifier) { $mission_id = $quest->{missions}->{$current_key}{mob_id}; last; } } } - # Fallback: when default lookup fails, use normalized hunt_id heuristics. + # Some servers can return mission updates keyed only by hunt identification. + # If direct lookup fails, map update by mission index from hunt_id. if (!defined $mission_id && defined $hunt_identifier) { my $mission_index = $hunt_identifier - ($mission->{questID} * 1000); + my @exact_candidates; + my @legacy_candidates; foreach my $current_key (keys %{$quest->{missions}}) { next unless exists $quest->{missions}->{$current_key}{mission_index}; - next unless $quest->{missions}->{$current_key}{mission_index} == $mission_index - || $quest->{missions}->{$current_key}{mission_index} == $mission_index - 1; - $mission_id = $current_key; - last; + my $current_index = $quest->{missions}->{$current_key}{mission_index}; + push @exact_candidates, $current_key if $current_index == $mission_index; + push @legacy_candidates, $current_key if $current_index == $mission_index - 1; } - } - # Some servers send hunt-only updates without a usable hunt_id for each mission. - # In multi-entry packets, fall back to packet order within each quest. - if (!defined $mission_id && $update_without_mob_id) { - my @index_candidates = grep { - exists $quest->{missions}->{$_}{mission_index} - && $quest->{missions}->{$_}{mission_index} == $quest_packet_index - } keys %{$quest->{missions}}; - $mission_id = $index_candidates[0] if @index_candidates == 1; - } - - # Last-resort for single-entry hunt-only updates: map by current attack target mob_id. - if (!defined $mission_id && $update_without_mob_id && $args->{mission_amount} == 1 - && AI::action() eq 'attack' && AI::args(0) && AI::args(0)->{ID}) { - my $target = $monstersList->getByID(AI::args(0)->{ID}); - if ($target && defined $target->{nameID}) { - my @target_candidates = grep { - exists $quest->{missions}->{$_}{mob_id} - && $quest->{missions}->{$_}{mob_id} == $target->{nameID} - } keys %{$quest->{missions}}; - $mission_id = $target_candidates[0] if @target_candidates == 1; + # Prefer exact mission_index match first. + if (@exact_candidates == 1) { + $mission_id = $exact_candidates[0]; + } elsif (!@exact_candidates && @legacy_candidates == 1) { + # Compatibility fallback for servers that report mission_index starting at 1. + $mission_id = $legacy_candidates[0]; + } elsif (@exact_candidates > 1 || @legacy_candidates > 1) { + debug TF("Quest mission update ignored due to ambiguous hunt mapping (quest: %d, hunt_id: %d, mission_index: %d)\n", + $mission->{questID}, $mission->{hunt_id}, $mission_index), "info"; } } - # For single-entry hunt-only updates, align slot by current quest progress when possible. - if ($update_without_mob_id && $args->{mission_amount} == 1) { - my @progress_candidates = grep { - exists $quest->{missions}->{$_}{mob_count} - && $mission->{mob_count} == $quest->{missions}->{$_}{mob_count} + 1 - } keys %{$quest->{missions}}; - $mission_id = $progress_candidates[0] if @progress_candidates == 1; - } - next unless defined $mission_id && exists $quest->{missions}->{$mission_id}; my $quest_mission = \%{$quest->{missions}->{$mission_id}}; - my $old_count = $quest_mission->{mob_count}; - my $old_goal = $quest_mission->{mob_goal}; $quest_mission->{mob_count} = $mission->{mob_count}; $quest_mission->{mob_goal} = $mission->{mob_goal}; - my $mission_changed = !defined $old_count || $old_count != $quest_mission->{mob_count} - || !defined $old_goal || $old_goal != $quest_mission->{mob_goal}; + debug "- MobID: $mission->{mob_id} - Name: $mission->{mob_name} - Count: $mission->{mob_count} - Goal: $mission->{mob_goal}\n", "info"; - if ($config{questDisplayStyle} && ($args->{mission_amount} == 1 || $mission_changed)) { + if ($config{questDisplayStyle}) { if ($config{questDisplayStyle} >= 2) { warning TF("[%s] Quest - defeated [%s] progress (%s/%s)\n", $quests_lut{$mission->{questID}} ? "$quests_lut{$mission->{questID}}{title} ($mission->{questID})" : $mission->{questID}, $quest_mission->{mob_name}, $quest_mission->{mob_count}, $quest_mission->{mob_goal}), "info"; } else {