From bc74f0f4e4556f380c255c4818c479efc6544935 Mon Sep 17 00:00:00 2001 From: Henrybk Date: Thu, 26 Dec 2024 12:16:11 -0300 Subject: [PATCH] Fix homunculus state crashes (#3920) * Fix homunculus state crashes * add homunculus_noinfo_dead and homunculus_noinfo_resting --- control/config.txt | 12 ++ src/AI/CoreLogic.pm | 83 ++++----- src/AI/Slave.pm | 238 ++++++++++++------------ src/AI/Slave/Homunculus.pm | 12 +- src/AI/SlaveManager.pm | 4 + src/Actor/You.pm | 20 +- src/Commands.pm | 18 +- src/Interface/Wx/StatView/Homunculus.pm | 8 +- src/Misc.pm | 27 ++- src/Network/Receive.pm | 106 +++++++---- 10 files changed, 298 insertions(+), 230 deletions(-) diff --git a/control/config.txt b/control/config.txt index 05fc49daaa..d75b469fd9 100644 --- a/control/config.txt +++ b/control/config.txt @@ -535,6 +535,8 @@ attackSkillSlot { homunculus_sp homunculus_dead homunculus_resting + homunculus_noinfo_dead + homunculus_noinfo_resting homunculus_onAction homunculus_notOnAction homunculus_whenIdle @@ -615,6 +617,8 @@ doCommand { homunculus_sp homunculus_dead homunculus_resting + homunculus_noinfo_dead + homunculus_noinfo_resting homunculus_onAction homunculus_notOnAction homunculus_whenIdle @@ -665,6 +669,8 @@ useSelf_skill { homunculus_sp homunculus_dead homunculus_resting + homunculus_noinfo_dead + homunculus_noinfo_resting homunculus_onAction homunculus_notOnAction homunculus_whenIdle @@ -719,6 +725,8 @@ partySkill { homunculus_sp homunculus_dead homunculus_resting + homunculus_noinfo_dead + homunculus_noinfo_resting homunculus_onAction homunculus_notOnAction homunculus_whenIdle @@ -819,6 +827,8 @@ equipAuto { homunculus_sp homunculus_dead homunculus_resting + homunculus_noinfo_dead + homunculus_noinfo_resting homunculus_onAction homunculus_notOnAction homunculus_whenIdle @@ -860,6 +870,8 @@ useSelf_item { homunculus_sp homunculus_dead homunculus_resting + homunculus_noinfo_dead + homunculus_noinfo_resting homunculus_onAction homunculus_notOnAction homunculus_whenIdle diff --git a/src/AI/CoreLogic.pm b/src/AI/CoreLogic.pm index dad42ce551..b9e106b47c 100644 --- a/src/AI/CoreLogic.pm +++ b/src/AI/CoreLogic.pm @@ -2889,6 +2889,7 @@ sub processAutoSkillUse { ##### PARTY-SKILL USE ##### sub processPartySkillUse { if (AI::isIdle || AI::is(qw(route mapRoute follow sitAuto take items_gather items_take attack move))){ + my $realMyPos = calcPosition($field, $char); my %party_skill; PARTYSKILL: for (my $i = 0; exists $config{"partySkill_$i"}; $i++) { @@ -2901,60 +2902,48 @@ sub processPartySkillUse { next if $ID eq '' || $ID eq $party_skill{owner}{ID}; if ($ID eq $accountID) { - # } elsif ($slavesList->getByID($ID)) { next if ((!$char->{slaves} || !$char->{slaves}{$ID}) && !$config{"partySkill_$i"."_notPartyOnly"}); next if (($char->{slaves}{$ID} ne $slavesList->getByID($ID)) && !$config{"partySkill_$i"."_notPartyOnly"}); } elsif ($playersList->getByID($ID)) { unless ($config{"partySkill_$i"."_notPartyOnly"}) { next unless $char->{party}{joined} && $char->{party}{users}{$ID}; - # party member should be online, otherwise it's another character on the same account (not in party) next unless $char->{party}{users}{$ID} && $char->{party}{users}{$ID}{online}; } } - my $player = Actor::get($ID); - next unless ( - UNIVERSAL::isa($player, 'Actor::You') - || UNIVERSAL::isa($player, 'Actor::Player') - || UNIVERSAL::isa($player, 'Actor::Slave') + next unless (UNIVERSAL::isa($player, 'Actor::You') || UNIVERSAL::isa($player, 'Actor::Player') || UNIVERSAL::isa($player, 'Actor::Slave')); + next unless ( # target check + !$config{"partySkill_$i"."_target"} + || existsInList($config{"partySkill_$i"."_target"}, $player->{name}) + || $player->{ID} eq $char->{ID} && existsInList($config{"partySkill_$i"."_target"}, '@main') + || $char->has_homunculus && $player->{ID} eq $char->{homunculus}{ID} && existsInList($config{"partySkill_$i"."_target"}, '@homunculus') + || $char->has_mercenary && $player->{ID} eq $char->{mercenary}{ID} && existsInList($config{"partySkill_$i"."_target"}, '@mercenary') ); - my $dist = $config{"partySkill_$i"."_dist"} || $config{partySkillDistance} || "0..8"; - if (defined($config{"partySkill_$i"."_dist"}) && defined($config{"partySkill_$i"."_maxDist"})) { $dist = $config{"partySkill_$i"."_dist"} . ".." . $config{"partySkill_$i"."_maxDist"};} - if ( - ( # range check - $party_skill{owner}{ID} eq $player->{ID} - || inRange(distance($party_skill{owner}{pos_to}, $player->{pos}), $dist) - ) - && ( # target check - !$config{"partySkill_$i"."_target"} - or existsInList($config{"partySkill_$i"."_target"}, $player->{name}) - or $player->{ID} eq $char->{ID} && existsInList($config{"partySkill_$i"."_target"}, '@main') - or $char->{homunculus} && $player->{ID} eq $char->{homunculus}{ID} && existsInList($config{"partySkill_$i"."_target"}, '@homunculus') - or $char->{mercenary} && $player->{ID} eq $char->{mercenary}{ID} && existsInList($config{"partySkill_$i"."_target"}, '@mercenary') - ) - && checkPlayerCondition("partySkill_$i"."_target", $ID) - ){ - $party_skill{ID} = $party_skill{skillObject}->getHandle; - $party_skill{lvl} = $config{"partySkill_$i"."_lvl"} || $char->getSkillLevel($party_skill{skillObject}); - $party_skill{target} = $player->{name}; - $party_skill{targetActor} = $player; - my $pos = $player->position; - $party_skill{x} = $pos->{x}; - $party_skill{y} = $pos->{y}; - $party_skill{targetID} = $ID; - $party_skill{maxCastTime} = $config{"partySkill_$i"."_maxCastTime"}; - $party_skill{minCastTime} = $config{"partySkill_$i"."_minCastTime"}; - $party_skill{isSelfSkill} = $config{"partySkill_$i"."_isSelfSkill"}; - $party_skill{prefix} = "partySkill_$i"; - # This is used by setSkillUseTimer() to set - # $ai_v{"partySkill_${i}_target_time"}{$targetID} - # when the skill is actually cast - $targetTimeout{$ID}{$party_skill{ID}} = $i; - last PARTYSKILL; - } - + my $party_skill_dist = $config{"partySkill_$i"."_dist"} || $config{partySkillDistance} || "0..8"; + if (defined($config{"partySkill_$i"."_dist"}) && defined($config{"partySkill_$i"."_maxDist"})) { $party_skill_dist = $config{"partySkill_$i"."_dist"} . ".." . $config{"partySkill_$i"."_maxDist"};} + my $realActorPos = calcPosition($field, $player); + my $distance = blockDistance($realMyPos, $realActorPos); + next unless ($party_skill{owner}{ID} eq $player->{ID} || inRange($distance, $party_skill_dist)); + next unless (checkPlayerCondition("partySkill_$i"."_target", $ID)); + + $party_skill{ID} = $party_skill{skillObject}->getHandle; + $party_skill{lvl} = $config{"partySkill_$i"."_lvl"} || $char->getSkillLevel($party_skill{skillObject}); + $party_skill{target} = $player->{name}; + $party_skill{targetActor} = $player; + $party_skill{x} = $realActorPos->{x}; + $party_skill{y} = $realActorPos->{y}; + $party_skill{targetID} = $ID; + $party_skill{maxCastTime} = $config{"partySkill_$i"."_maxCastTime"}; + $party_skill{minCastTime} = $config{"partySkill_$i"."_minCastTime"}; + $party_skill{isSelfSkill} = $config{"partySkill_$i"."_isSelfSkill"}; + $party_skill{prefix} = "partySkill_$i"; + # This is used by setSkillUseTimer() to set + # $ai_v{"partySkill_${i}_target_time"}{$targetID} + # when the skill is actually cast + $targetTimeout{$ID}{$party_skill{ID}} = $i; + last PARTYSKILL; } } @@ -2965,9 +2954,11 @@ sub processPartySkillUse { if ($char->{party}{joined} && $char->{party}{users}{$party_skill{targetID}} && $char->{party}{users}{$party_skill{targetID}}{hp}) { $hp_diff = $char->{party}{users}{$party_skill{targetID}}{hp_max} - $char->{party}{users}{$party_skill{targetID}}{hp}; - } elsif ($char->{mercenary} && $char->{mercenary}{hp} && $char->{mercenary}{hp_max}) { + + } elsif ($char->has_mercenary && $party_skill{targetID} eq $char->{mercenary}{ID} && $char->{mercenary}{hp} && $char->{mercenary}{hp_max}) { $hp_diff = $char->{mercenary}{hp_max} - $char->{mercenary}{hp}; $modifier /= 2; + } else { if ($players{$party_skill{targetID}}) { $hp_diff = -$players{$party_skill{targetID}}{deltaHp}; @@ -3127,9 +3118,9 @@ sub processAutoAttack { if ($config{'tankMode'}) { for (@$playersList, @$slavesList) { if ( - $config{tankModeTarget} eq $_->{name} - or $char->{homunculus} && $config{tankModeTarget} eq '@homunculus' && $_->{ID} eq $char->{homunculus}{ID} - or $char->{mercenary} && $config{tankModeTarget} eq '@mercenary' && $_->{ID} eq $char->{mercenary}{ID} + ($config{tankModeTarget} eq $_->{name}) + || ($char->has_homunculus && $config{tankModeTarget} eq '@homunculus' && $_->{ID} eq $char->{homunculus}{ID}) + || ($char->has_mercenary && $config{tankModeTarget} eq '@mercenary' && $_->{ID} eq $char->{mercenary}{ID}) ) { $foundTankee = 1; last; diff --git a/src/AI/Slave.pm b/src/AI/Slave.pm index 3a3d560e0a..ed1c8e235f 100644 --- a/src/AI/Slave.pm +++ b/src/AI/Slave.pm @@ -363,6 +363,9 @@ sub processClientSuspend { ##### AUTO-ATTACK ##### sub processAutoAttack { my $slave = shift; + + return if $slave->inQueue("attack"); + # The auto-attack logic is as follows: # 1. Generate a list of monsters that we are allowed to attack. # 2. Pick the "best" monster out of that list, and attack it. @@ -371,57 +374,57 @@ sub processAutoAttack { return if ($config{$slave->{configPrefix}.'attackAuto'} && $config{$slave->{configPrefix}.'attackAuto'} eq -1); return if (!$field); - if ( - ($slave->isIdle || $slave->is(qw/route/)) - && ( - AI::isIdle - || AI::is(qw(follow sitAuto attack skill_use)) - || (AI::action eq "route" && AI::action(1) eq "attack") - || (AI::action eq "move" && AI::action(2) eq "attack") - || ($config{$slave->{configPrefix}.'attackAuto_duringItemsTake'} && AI::is(qw(take items_gather items_take))) - || ($config{$slave->{configPrefix}.'attackAuto_duringRandomWalk'} && AI::is('route') && AI::args()->{isRandomWalk})) - && timeOut($timeout{$slave->{ai_attack_auto_timeout}}) - && $slave->{master_dist} <= $config{$slave->{configPrefix}.'followDistanceMax'} - && ((AI::action ne "move" && AI::action ne "route") || blockDistance($char->{pos_to}, $slave->{pos_to}) <= $config{$slave->{configPrefix}.'followDistanceMax'}) - && (!$config{$slave->{configPrefix}.'attackAuto_notInTown'} || !$field->isCity) - && ($config{$slave->{configPrefix}.'attackAuto_inLockOnly'} <= 1 || $field->baseName eq $config{'lockMap'}) - && (!$config{$slave->{configPrefix}.'attackAuto_notWhile_storageAuto'} || !AI::inQueue("storageAuto")) - && (!$config{$slave->{configPrefix}.'attackAuto_notWhile_buyAuto'} || !AI::inQueue("buyAuto")) - && (!$config{$slave->{configPrefix}.'attackAuto_notWhile_sellAuto'} || !AI::inQueue("sellAuto")) - ) { - - # If we're in tanking mode, only attack something if the person we're tanking for is on screen. - my $foundTankee; - if ($config{$slave->{configPrefix}.'tankMode'}) { - if ($config{$slave->{configPrefix}.'tankModeTarget'} eq $char->{name}) { - $foundTankee = 1; - } else { - foreach (@playersID) { - next if (!$_); - if ($config{$slave->{configPrefix}.'tankModeTarget'} eq $players{$_}{'name'}) { - $foundTankee = 1; - last; - } + next unless ($slave->isIdle || $slave->is(qw/route/)); + + next unless ( + AI::isIdle || + AI::is(qw(follow sitAuto attack skill_use)) || + (AI::action eq "route" && AI::action(1) eq "attack") || + (AI::action eq "move" && AI::action(2) eq "attack") || + ($config{$slave->{configPrefix}.'attackAuto_duringItemsTake'} && AI::is(qw(take items_gather items_take))) || + ($config{$slave->{configPrefix}.'attackAuto_duringRandomWalk'} && AI::is('route') && AI::args()->{isRandomWalk}) + ); + next unless (timeOut($timeout{$slave->{ai_attack_auto_timeout}})); + next unless ($slave->{master_dist} <= $config{$slave->{configPrefix}.'followDistanceMax'}); + #next unless ((AI::action ne "move" && AI::action ne "route") || blockDistance($char->{pos_to}, $slave->{pos_to}) <= $config{$slave->{configPrefix}.'followDistanceMax'}); + next unless (!$config{$slave->{configPrefix}.'attackAuto_notInTown'} || !$field->isCity); + next unless ($config{$slave->{configPrefix}.'attackAuto_inLockOnly'} <= 1 || $field->baseName eq $config{'lockMap'}); + next unless (!$config{$slave->{configPrefix}.'attackAuto_notWhile_storageAuto'} || !AI::inQueue("storageAuto")); + next unless (!$config{$slave->{configPrefix}.'attackAuto_notWhile_buyAuto'} || !AI::inQueue("buyAuto")); + next unless (!$config{$slave->{configPrefix}.'attackAuto_notWhile_sellAuto'} || !AI::inQueue("sellAuto")); + + # If we're in tanking mode, only attack something if the person we're tanking for is on screen. + my $foundTankee; + if ($config{$slave->{configPrefix}.'tankMode'}) { + if ($config{$slave->{configPrefix}.'tankModeTarget'} eq $char->{name}) { + $foundTankee = 1; + } else { + foreach (@playersID) { + next if (!$_); + if ($config{$slave->{configPrefix}.'tankModeTarget'} eq $players{$_}{'name'}) { + $foundTankee = 1; + last; } } } + } - my $attackTarget; - my $priorityAttack; + my $attackTarget; + my $priorityAttack; - if (!$config{$slave->{configPrefix}.'tankMode'} || $foundTankee) { - # This variable controls how far monsters must be away from portals and players. - my $portalDist = $config{'attackMinPortalDistance'} || 0; # Homun do not have effect on portals - my $playerDist = $config{'attackMinPlayerDistance'}; - $playerDist = 1 if ($playerDist < 1); - - my $routeIndex = $slave->findAction("route"); - my $attackOnRoute; - if (defined $routeIndex) { - $attackOnRoute = $slave->args($routeIndex)->{attackOnRoute}; - } else { - $attackOnRoute = 2; - } + if (!$config{$slave->{configPrefix}.'tankMode'} || $foundTankee) { + # This variable controls how far monsters must be away from portals and players. + my $portalDist = $config{'attackMinPortalDistance'} || 0; # Homun do not have effect on portals + my $playerDist = $config{'attackMinPlayerDistance'}; + $playerDist = 1 if ($playerDist < 1); + + my $routeIndex = $slave->findAction("route"); + my $attackOnRoute; + if (defined $routeIndex) { + $attackOnRoute = $slave->args($routeIndex)->{attackOnRoute}; + } else { + $attackOnRoute = 2; + } ### Step 1: Generate a list of all monsters that we are allowed to attack. ### my @aggressives; @@ -429,93 +432,92 @@ sub processAutoAttack { my @cleanMonsters; my $myPos = calcPosition($slave); - # List aggressive monsters - my $party = $config{$slave->{configPrefix}.'attackAuto_party'} ? 1 : 0; - @aggressives = AI::ai_slave_getAggressives($slave, 1, $party) if ($config{$slave->{configPrefix}.'attackAuto'} && $attackOnRoute); + # List aggressive monsters + my $party = $config{$slave->{configPrefix}.'attackAuto_party'} ? 1 : 0; + @aggressives = AI::ai_slave_getAggressives($slave, 1, $party) if ($config{$slave->{configPrefix}.'attackAuto'} && $attackOnRoute); - # There are two types of non-aggressive monsters. We generate two lists: - foreach (@monstersID) { - next if (!$_ || !slave_checkMonsterCleanness($slave, $_)); - my $monster = $monsters{$_}; + # There are two types of non-aggressive monsters. We generate two lists: + foreach (@monstersID) { + next if (!$_ || !slave_checkMonsterCleanness($slave, $_)); + my $monster = $monsters{$_}; - # Never attack monsters that we failed to get LOS with - next if (!timeOut($monster->{attack_failedLOS}, $timeout{ai_attack_failedLOS}{timeout})); + # Never attack monsters that we failed to get LOS with + next if (!timeOut($monster->{attack_failedLOS}, $timeout{ai_attack_failedLOS}{timeout})); my $pos = calcPosition($monster); my $master_pos = $char->position; next if (blockDistance($master_pos, $pos) > ($config{$slave->{configPrefix}.'followDistanceMax'} + $config{$slave->{configPrefix}.'attackMaxDistance'})); - # List monsters that master and other slaves are attacking - if ( - $config{$slave->{configPrefix}.'attackAuto_party'} - && $attackOnRoute - && timeOut($monster->{$slave->{ai_attack_failed_timeout}}, $timeout{ai_attack_unfail}{timeout}) - && ( - ($monster->{missedFromYou} && $config{$slave->{configPrefix}.'attackAuto_party'} != 2) - || ($monster->{dmgFromYou} && $config{$slave->{configPrefix}.'attackAuto_party'} != 2) - || ($monster->{castOnByYou} && $config{$slave->{configPrefix}.'attackAuto_party'} != 2) - || $monster->{dmgToYou} - || $monster->{missedYou} - || $monster->{castOnToYou} - || (scalar(grep { isMySlaveID($_, $slave->{ID}) } keys %{$monster->{missedFromPlayer}}) && $config{$slave->{configPrefix}.'attackAuto_party'} != 2) - || (scalar(grep { isMySlaveID($_, $slave->{ID}) } keys %{$monster->{dmgFromPlayer}}) && $config{$slave->{configPrefix}.'attackAuto_party'} != 2) - || (scalar(grep { isMySlaveID($_, $slave->{ID}) } keys %{$monster->{castOnByPlayer}}) && $config{$slave->{configPrefix}.'attackAuto_party'} != 2) - || scalar(grep { isMySlaveID($_, $slave->{ID}) } keys %{$monster->{missedToPlayer}}) - || scalar(grep { isMySlaveID($_, $slave->{ID}) } keys %{$monster->{dmgToPlayer}}) - || scalar(grep { isMySlaveID($_, $slave->{ID}) } keys %{$monster->{castOnToPlayer}}) - ) - ) { - push @partyMonsters, $_; - next; - } + # List monsters that master and other slaves are attacking + if ( + $config{$slave->{configPrefix}.'attackAuto_party'} + && $attackOnRoute + && timeOut($monster->{$slave->{ai_attack_failed_timeout}}, $timeout{ai_attack_unfail}{timeout}) + && ( + ($monster->{missedFromYou} && $config{$slave->{configPrefix}.'attackAuto_party'} != 2) + || ($monster->{dmgFromYou} && $config{$slave->{configPrefix}.'attackAuto_party'} != 2) + || ($monster->{castOnByYou} && $config{$slave->{configPrefix}.'attackAuto_party'} != 2) + || $monster->{dmgToYou} + || $monster->{missedYou} + || $monster->{castOnToYou} + || (scalar(grep { isMySlaveID($_, $slave->{ID}) } keys %{$monster->{missedFromPlayer}}) && $config{$slave->{configPrefix}.'attackAuto_party'} != 2) + || (scalar(grep { isMySlaveID($_, $slave->{ID}) } keys %{$monster->{dmgFromPlayer}}) && $config{$slave->{configPrefix}.'attackAuto_party'} != 2) + || (scalar(grep { isMySlaveID($_, $slave->{ID}) } keys %{$monster->{castOnByPlayer}}) && $config{$slave->{configPrefix}.'attackAuto_party'} != 2) + || scalar(grep { isMySlaveID($_, $slave->{ID}) } keys %{$monster->{missedToPlayer}}) + || scalar(grep { isMySlaveID($_, $slave->{ID}) } keys %{$monster->{dmgToPlayer}}) + || scalar(grep { isMySlaveID($_, $slave->{ID}) } keys %{$monster->{castOnToPlayer}}) + ) + ) { + push @partyMonsters, $_; + next; + } + + ### List normal, non-aggressive monsters. ### - ### List normal, non-aggressive monsters. ### - - # Ignore monsters that - # - Are inside others' area spells (this includes being trapped). - # - Are moving towards other players. - next if (objectInsideSpell($monster) || objectIsMovingTowardsPlayer($monster)); - - my $safe = 1; - if ($config{$slave->{configPrefix}.'attackAuto_onlyWhenSafe'}) { - foreach (@playersID) { - next if ($_ eq $slave->{ID}); - if ($_ && !$char->{party}{users}{$_}) { - $safe = 0; - last; - } + # Ignore monsters that + # - Are inside others' area spells (this includes being trapped). + # - Are moving towards other players. + next if (objectInsideSpell($monster) || objectIsMovingTowardsPlayer($monster)); + + my $safe = 1; + if ($config{$slave->{configPrefix}.'attackAuto_onlyWhenSafe'}) { + foreach (@playersID) { + next if ($_ eq $slave->{ID}); + if ($_ && !$char->{party}{users}{$_}) { + $safe = 0; + last; } } - - my $control = mon_control($monster->{name}, $monster->{nameID}); - if ($config{$slave->{configPrefix}.'attackAuto'} >= 2 - && ($control->{attack_auto} == 1 || $control->{attack_auto} == 3) - && $attackOnRoute >= 2 && $safe - && !positionNearPlayer($pos, $playerDist) && !positionNearPortal($pos, $portalDist) - && !$monster->{dmgFromYou} - && timeOut($monster->{$slave->{ai_attack_failed_timeout}}, $timeout{ai_attack_unfail}{timeout})) { - push @cleanMonsters, $_; - } } + + my $control = mon_control($monster->{name}, $monster->{nameID}); + if ($config{$slave->{configPrefix}.'attackAuto'} >= 2 + && ($control->{attack_auto} == 1 || $control->{attack_auto} == 3) + && $attackOnRoute >= 2 && $safe + && !positionNearPlayer($pos, $playerDist) && !positionNearPortal($pos, $portalDist) + && !$monster->{dmgFromYou} + && timeOut($monster->{$slave->{ai_attack_failed_timeout}}, $timeout{ai_attack_unfail}{timeout})) { + push @cleanMonsters, $_; + } + } - ### Step 2: Pick out the "best" monster ### + ### Step 2: Pick out the "best" monster ### - # We define whether we should attack only monsters in LOS or not - my $checkLOS = $config{$slave->{configPrefix}.'attackCheckLOS'}; - my $canSnipe = $config{$slave->{configPrefix}.'attackCanSnipe'}; - $attackTarget = getBestTarget(\@aggressives, $checkLOS, $canSnipe) || - getBestTarget(\@partyMonsters, $checkLOS, $canSnipe) || - getBestTarget(\@cleanMonsters, $checkLOS, $canSnipe); - } + # We define whether we should attack only monsters in LOS or not + my $checkLOS = $config{$slave->{configPrefix}.'attackCheckLOS'}; + my $canSnipe = $config{$slave->{configPrefix}.'attackCanSnipe'}; + $attackTarget = getBestTarget(\@aggressives, $checkLOS, $canSnipe) || + getBestTarget(\@partyMonsters, $checkLOS, $canSnipe) || + getBestTarget(\@cleanMonsters, $checkLOS, $canSnipe); + } - # If an appropriate monster's found, attack it. If not, wait ai_attack_auto secs before searching again. - if ($attackTarget) { - $slave->setSuspend(0); - $slave->attack($attackTarget, $priorityAttack); - } else { - $timeout{$slave->{ai_attack_auto_timeout}}{time} = time; - } + # If an appropriate monster's found, attack it. If not, wait ai_attack_auto secs before searching again. + if ($attackTarget) { + $slave->setSuspend(0); + $slave->attack($attackTarget, $priorityAttack); + } else { + $timeout{$slave->{ai_attack_auto_timeout}}{time} = time; } #Benchmark::end("ai_homunculus_autoAttack") if DEBUG; diff --git a/src/AI/Slave/Homunculus.pm b/src/AI/Slave/Homunculus.pm index 94bda4f6a5..cfef9c3e8b 100644 --- a/src/AI/Slave/Homunculus.pm +++ b/src/AI/Slave/Homunculus.pm @@ -15,12 +15,16 @@ sub iterate { my $slave = shift; # homunculus is in rest - if ($slave->{vaporized}) { - #message TF("Slave %s vaporized\n", $slave), 'slave'; + if ($slave->{homunculus_info}{vaporized}) { + message TF("Slave %s vaporized, removing from slave manager\n", $slave), 'slave'; + AI::SlaveManager::removeSlave($slave); + return; # homunculus is dead / not present - } elsif ($slave->{dead}) { - #message TF("Slave %s dead\n", $slave), 'slave'; + } elsif ($slave->{homunculus_info}{dead}) { + message TF("Slave %s dead, removing from slave manager\n", $slave), 'slave'; + AI::SlaveManager::removeSlave($slave); + return; # homunculus is alive } elsif ($slave->{appear_time} && $field->baseName eq $slave->{map}) { diff --git a/src/AI/SlaveManager.pm b/src/AI/SlaveManager.pm index 2c382bf19f..87a5e3a5b0 100644 --- a/src/AI/SlaveManager.pm +++ b/src/AI/SlaveManager.pm @@ -31,6 +31,10 @@ sub addSlave { $actor->{ai_dance_attack_melee_timeout} = 'ai_homunculus_dance_attack_melee'; $actor->{ai_attack_waitAfterKill_timeout} = 'ai_homunculus_attack_waitAfterKill'; $actor->{ai_attack_failed_timeout} = 'homunculus_attack_failed'; + if (!exists $char->{homunculus_info}) { + $char->{homunculus_info} = {}; + } + $actor->{homunculus_info} = $char->{homunculus_info}; # A reference bless $actor, 'AI::Slave::Homunculus'; } elsif ($actor->isa("Actor::Slave::Mercenary")) { diff --git a/src/Actor/You.pm b/src/Actor/You.pm index 970ec516d9..02fb784df0 100644 --- a/src/Actor/You.pm +++ b/src/Actor/You.pm @@ -66,6 +66,11 @@ use Utils; # If the character has a homunculus, and the homunculus is currently online, then this # member points to character's homunculus object. +## +# $char->{homunculus_info} +# +# If the has a homunculus (including dead or vaporized) this can hold some information about it + ## # Bytes $char->{charID} # @@ -89,17 +94,26 @@ sub new { sub has_homunculus { my ($self) = @_; - if ($self && $self->{homunculus} && $self->{slaves} && scalar(keys(%{$self->{slaves}})) && $self->{homunculus}{ID} && exists $self->{slaves}{$self->{homunculus}{ID}}) { + if ($self && + exists $self->{homunculus_info} && + exists $self->{homunculus} && + $self->{slaves} && + scalar(keys(%{$self->{slaves}})) && + $self->{homunculus}{ID} && + exists $self->{slaves}{$self->{homunculus}{ID}} && + (!exists $char->{homunculus_info}{dead} || ! $char->{homunculus_info}{dead}) && + (!exists $char->{homunculus_info}{vaporized} || ! $char->{homunculus_info}{vaporized}) + ) { return 1; } - + return 0; } sub has_mercenary { my ($self) = @_; - if ($self && $self->{mercenary} && $self->{slaves} && scalar(keys(%{$self->{slaves}})) && $self->{mercenary}{ID} && exists $self->{slaves}{$self->{mercenary}{ID}}) { + if ($self && exists $self->{mercenary} && $self->{mercenary} && $self->{slaves} && scalar(keys(%{$self->{slaves}})) && $self->{mercenary}{ID} && exists $self->{slaves}{$self->{mercenary}{ID}}) { return 1; } diff --git a/src/Commands.pm b/src/Commands.pm index f2eb8e6764..89dba0e76e 100644 --- a/src/Commands.pm +++ b/src/Commands.pm @@ -2896,22 +2896,28 @@ sub cmdSlave { my $slave; if ($cmd eq 'homun') { - $slave = $char->{homunculus}; + if ($char->has_homunculus && $char->{homunculus}{appear_time}) { + $slave = $char->{homunculus}; + } else { + error T("Error: No slave detected.\n"); + } } elsif ($cmd eq 'merc') { $slave = $char->{mercenary}; + if ($char->has_mercenary && $char->{mercenary}{appear_time}) { + $slave = $char->{mercenary}; + } else { + error T("Error: No slave detected.\n"); + } } else { error T("Error: Unknown command in cmdSlave\n"); } my $string = $cmd; - if (!$slave || !$slave->{appear_time}) { - error T("Error: No slave detected.\n"); - - } elsif ($slave->isa("AI::Slave::Homunculus") && $slave->{vaporized}) { + if ($slave->isa("AI::Slave::Homunculus") && $slave->{homunculus_info}{vaporized}) { my $skill = new Skill(handle => 'AM_CALLHOMUN'); error TF("Homunculus is in rest, use skills '%s' (ss %d).\n", $skill->getName, $skill->getIDN); - } elsif ($slave->isa("AI::Slave::Homunculus") && $slave->{dead}) { + } elsif ($slave->isa("AI::Slave::Homunculus") && $slave->{homunculus_info}{dead}) { my $skill = new Skill(handle => 'AM_RESURRECTHOMUN'); error TF("Homunculus is dead, use skills '%s' (ss %d).\n", $skill->getName, $skill->getIDN); diff --git a/src/Interface/Wx/StatView/Homunculus.pm b/src/Interface/Wx/StatView/Homunculus.pm index 8c0a8a6e62..c562f759b5 100644 --- a/src/Interface/Wx/StatView/Homunculus.pm +++ b/src/Interface/Wx/StatView/Homunculus.pm @@ -59,18 +59,18 @@ sub update { return unless $conState == Network::IN_GAME; $self->set ('feed', - $char->{homunculus} && $char->{homunculus}{state} == HO_STATE_ALIVE + $char->has_homunculus ); $self->set ('vaporize', - $char->{homunculus} && $char->{homunculus}{state} == HO_STATE_ALIVE + $char->has_homunculus && $char->{skills}{(HO_SKILL_VAPORIZE)} && $char->{skills}{(HO_SKILL_VAPORIZE)}{lv} ); $self->set ('call', - (!$char->{homunculus} || !defined $char->{homunculus}{state} || $char->{homunculus}{state} == HO_STATE_REST) + (!$char->has_homunculus || !exists $char->{homunculus_info} || (exists $char->{homunculus_info} && $char->{homunculus_info}{vaporized})) && $char->{skills}{(HO_SKILL_CALL)} && $char->{skills}{(HO_SKILL_CALL)}{lv} ); $self->set ('resurrect', - (!$char->{homunculus} || (!defined $char->{homunculus}{state} && $char->{homunculus}{state} == HO_STATE_DEAD)) + (!$char->has_homunculus || !exists $char->{homunculus_info} || (exists $char->{homunculus_info} && $char->{homunculus_info}{dead})) && $char->{skills}{(HO_SKILL_RESURRECT)} && $char->{skills}{(HO_SKILL_RESURRECT)}{lv} ); diff --git a/src/Misc.pm b/src/Misc.pm index 024e13ae58..4eef1a14bb 100644 --- a/src/Misc.pm +++ b/src/Misc.pm @@ -1500,9 +1500,10 @@ sub checkFollowMode { sub isMySlaveID { my ($ID, $exclude) = @_; + return 0 if (defined $exclude && $ID eq $exclude); return 0 unless ($char); + return 1 if (exists $char->{homunculus} && exists $char->{homunculus}{ID} && $char->{homunculus}{ID} eq $ID); return 0 unless ($char->{slaves}); - return 0 if (defined $exclude && $ID eq $exclude); return 0 unless (exists $char->{slaves}{$ID}); return 1; } @@ -4684,16 +4685,24 @@ sub checkSelfCondition { return 0 if ($char->{homunculus}->isIdle); } - if ($config{$prefix."_homunculus_dead"}) { - return 0 if ($has_homunculus); - return 0 unless ($char->{homunculus}); - return 0 unless ($char->{homunculus}{dead}); + if ($config{$prefix."_homunculus_dead"} =~ /\S/) { + return 0 if (exists $char->{homunculus_info} && $config{$prefix."_homunculus_dead"} && $char->{homunculus_info}{dead} == 0); + return 0 if (exists $char->{homunculus_info} && !$config{$prefix."_homunculus_dead"} && $char->{homunculus_info}{dead} == 1); + } + + if ($config{$prefix."_homunculus_resting"} =~ /\S/) { + return 0 if (exists $char->{homunculus_info} && $config{$prefix."_homunculus_resting"} && $char->{homunculus_info}{vaporized} == 0); + return 0 if (exists $char->{homunculus_info} && !$config{$prefix."_homunculus_resting"} && $char->{homunculus_info}{vaporized} == 1); + } + + if ($config{$prefix."_homunculus_noinfo_dead"} =~ /\S/) { + return 0 if ($config{$prefix."_homunculus_noinfo_dead"} && exists $char->{homunculus_info} && exists $char->{homunculus_info}{dead} && defined $char->{homunculus_info}{dead}); + return 0 if (!$config{$prefix."_homunculus_noinfo_dead"} && (!exists $char->{homunculus_info} || !exists $char->{homunculus_info}{dead} || !defined $char->{homunculus_info}{dead})); } - if ($config{$prefix."_homunculus_resting"}) { - return 0 if ($has_homunculus); - return 0 unless ($char->{homunculus}); - return 0 unless ($char->{homunculus}{vaporized}); + if ($config{$prefix."_homunculus_noinfo_resting"} =~ /\S/) { + return 0 if ($config{$prefix."_homunculus_noinfo_resting"} && exists $char->{homunculus_info} && exists $char->{homunculus_info}{vaporized} && defined $char->{homunculus_info}{vaporized}); + return 0 if (!$config{$prefix."_homunculus_noinfo_resting"} && (!exists $char->{homunculus_info} || !exists $char->{homunculus_info}{vaporized} || !defined $char->{homunculus_info}{vaporized})); } my $has_mercenary = $char->has_mercenary; diff --git a/src/Network/Receive.pm b/src/Network/Receive.pm index 0d4b2dc457..8f373d33c0 100644 --- a/src/Network/Receive.pm +++ b/src/Network/Receive.pm @@ -2519,10 +2519,9 @@ sub actor_died_or_disappeared { my $slave = $slavesList->getByID($ID); if ($args->{type} == 1) { message TF("Slave Died: %s (%d) %s\n", $slave->name, $slave->{binID}, $slave->{actorType}); - $slave->{state} = 0; if (isMySlaveID($ID)) { - $slave->{dead} = 1; if ($slave->isa("AI::Slave::Homunculus") || $slave->isa("Actor::Slave::Homunculus")) { + $char->{homunculus_info}{dead} = 1; AI::SlaveManager::removeSlave($slave) if ($char->has_homunculus); } elsif ($slave->isa("AI::Slave::Mercenary") || $slave->isa("Actor::Slave::Mercenary")) { @@ -2885,9 +2884,10 @@ sub reconstruct_minimap_indicator { # 0ba4 .24B .B .W .W .W .W .W .W .W .W .W .W .W .L .L .L .L .eL .eL .W .W (ZC_PROPERTY_HOMUN4) sub homunculus_property { my ($self, $args) = @_; - - my $slave = $char->{homunculus} or return; - + + return 0 unless enforce_homun_state(); + + my $slave = $char->{homunculus}; $slave->{name} = bytesToString($args->{name}); slave_calcproperty_handler($slave, $args); @@ -2908,77 +2908,78 @@ sub homunculus_property { } } +sub enforce_homun_state { + return 0 unless ($char); + if (exists $char->{homunculus} && defined $char->{homunculus}) { + return 1; + } else { + debug "[Homunculus] Received homunculus property without the homunculus objetive existing, creating a temporary one.\n"; + $char->{homunculus} = Actor::new('Actor::Slave::Homunculus'); + return 1; + } +} + sub homunculus_state_handler { my ($slave, $args) = @_; - # Homunculus states: - # 0 - alive and unnamed + # Homunculus states bit: + # 0 - alive, unnamed and not vaporized + # 1 - named # 2 - rest # 4 - dead - return unless $char->{homunculus}; - $char->{homunculus}->clear(); - if (!defined $slave->{state}) { if ($args->{state} & 1) { - $char->{homunculus}{renameflag} = 1; + $char->{homunculus_info}{renameflag} = 1; message T("Your Homunculus has already been renamed\n"), 'homunculus'; } else { - $char->{homunculus}{renameflag} = 0; + $char->{homunculus_info}{renameflag} = 0; message T("Your Homunculus has not been renamed\n"), 'homunculus'; } if ($args->{state} & 2) { - $char->{homunculus}{vaporized} = 1; - AI::SlaveManager::removeSlave($char->{homunculus}) if ($char->has_homunculus); + $char->{homunculus_info}{vaporized} = 1; message T("Your Homunculus is vaporized\n"), 'homunculus'; } else { - $char->{homunculus}{vaporized} = 0; - AI::SlaveManager::addSlave($char->{homunculus}) if (!$char->has_homunculus); + $char->{homunculus_info}{vaporized} = 0; message T("Your Homunculus is not vaporized\n"), 'homunculus'; } if ($args->{state} & 4) { - $char->{homunculus}{dead} = 0; - AI::SlaveManager::addSlave($char->{homunculus}) if (!$char->has_homunculus); + $char->{homunculus_info}{dead} = 0; message T("Your Homunculus is not dead\n"), 'homunculus'; } else { - $char->{homunculus}{dead} = 1; - AI::SlaveManager::removeSlave($char->{homunculus}) if ($char->has_homunculus); + $char->{homunculus_info}{dead} = 1; message T("Your Homunculus is dead\n"), 'homunculus'; } } elsif (defined $slave->{state} && $slave->{state} != $args->{state}) { if (($args->{state} & 1) && !($slave->{state} & 1)) { - $char->{homunculus}{renameflag} = 1; + $char->{homunculus_info}{renameflag} = 1; message T("Your Homunculus was renamed\n"), 'homunculus'; } if (($args->{state} & 2) && !($slave->{state} & 2)) { - $char->{homunculus}{vaporized} = 1; - AI::SlaveManager::removeSlave($char->{homunculus}) if ($char->has_homunculus); + $char->{homunculus_info}{vaporized} = 1; message T("Your Homunculus was vaporized!\n"), 'homunculus'; } if (($args->{state} & 4) && !($slave->{state} & 4)) { - $char->{homunculus}{dead} = 0; - AI::SlaveManager::addSlave($char->{homunculus}) if (!$char->has_homunculus); + $char->{homunculus_info}{dead} = 0; message T("Your Homunculus was resurrected!\n"), 'homunculus'; } if (!($args->{state} & 1) && ($slave->{state} & 1)) { - $char->{homunculus}{renameflag} = 0; + $char->{homunculus_info}{renameflag} = 0; message T("Your Homunculus was un-renamed? lol\n"), 'homunculus'; } if (!($args->{state} & 2) && ($slave->{state} & 2)) { - $char->{homunculus}{vaporized} = 0; - AI::SlaveManager::addSlave($char->{homunculus}) if (!$char->has_homunculus); + $char->{homunculus_info}{vaporized} = 0; message T("Your Homunculus was recalled!\n"), 'homunculus'; } if (!($args->{state} & 4) && ($slave->{state} & 4)) { - $char->{homunculus}{dead} = 1; - AI::SlaveManager::removeSlave($char->{homunculus}) if ($char->has_homunculus); + $char->{homunculus_info}{dead} = 1; message T("Your Homunculus died!\n"), 'homunculus'; } } @@ -3006,20 +3007,24 @@ sub homunculus_info { my ($self, $args) = @_; debug "homunculus_info type: $args->{type}\n", "homunculus"; if ($args->{state} == HO_PRE_INIT) { - my $state = $char->{homunculus}{state} - if ($char->{homunculus} && $char->{homunculus}{ID} && $char->{homunculus}{ID} ne $args->{ID}); # Some servers won't send 'homunculus_property' after a teleport, so we don't delete $char->{homunculus} object + if ($char->{homunculus_info}{dead} == 1) { + debug "[Homunculus] We received a homunculus_info packet while our homunculus is dead, assume it was resurrected.\n"; + $char->{homunculus_info}{dead} = 0; + } + $char->{homunculus} = Actor::get($args->{ID}) if ($char->{homunculus}{ID} ne $args->{ID}); - - $char->{homunculus}{state} = $state if (defined $state); $char->{homunculus}{map} = $field->baseName; unless ($char->{slaves}{$char->{homunculus}{ID}}) { if ($char->{homunculus}->isa('AI::Slave::Homunculus')) { # After a teleport the homunculus object is still AI::Slave::Homunculus, but AI::SlaveManager::addSlave requires it to be Actor::Slave::Homunculus, so we change it back bless $char->{homunculus}, 'Actor::Slave::Homunculus'; } - AI::SlaveManager::addSlave($char->{homunculus}) if (!$char->has_homunculus); + if (!$char->has_homunculus) { + debug "[Homunculus] Adding homunculus to SlaveManager after homunculus_info packet.\n"; + AI::SlaveManager::addSlave($char->{homunculus}); + } $char->{homunculus}{appear_time} = time; } } elsif ($args->{state} == HO_RELATIONSHIP_CHANGED) { @@ -11311,12 +11316,16 @@ sub resurrection { } if (isMySlaveID($targetID)) { - my $slave = $slavesList->getByID($targetID); - if (defined $slave && ($slave->isa("AI::Slave::Homunculus") || $slave->isa("Actor::Slave::Homunculus"))) { + enforce_homun_state(); + my $slave = Actor::get($targetID); + if ($slave->isa("AI::Slave::Homunculus") || $slave->isa("Actor::Slave::Homunculus")) { message TF("Slave Resurrected: %s\n", $slave); - $slave->{state} = 4; - $slave->{dead} = 0; - AI::SlaveManager::addSlave($slave) if (!$char->has_homunculus); + $char->{homunculus_info}{dead} = 0; + if (!$char->has_homunculus) { + debug "[Homunculus] Adding homunculus to SlaveManager after homunculus_info packet.\n"; + bless $char->{homunculus}, 'Actor::Slave::Homunculus'; + AI::SlaveManager::addSlave($slave); + } } } message TF("%s has been resurrected\n", getActorName($targetID)), "info"; @@ -11766,6 +11775,23 @@ sub skill_use_failed { Plugins::callHook('packet_skillfail', \%hookArgs); warning(TF("Skill %s failed: %s (error number %s)\n", Skill->new(idn => $args->{skillID})->getName(), $errorMessage, $args->{cause}), "skill") if ($hookArgs{warn}); + + # Ressurect Homunculus failed - which means we have no dead homunculus + if ($args->{skillID} == 247 && $args->{cause} == 0) { + debug "[Homunculus] Ressurect Homunculus failed - which means we have no dead homunculus.\n"; + $char->{homunculus_info}{dead} = 0; + } + + # Call Homunculus failed - which means we have no vaporized homunculus + if ($args->{skillID} == 243) { + if ($args->{cause} == 0) { + debug "[Homunculus] Call Homunculus failed - which means we have no vaporized homunculus.\n"; + $char->{homunculus_info}{vaporized} = 0; + } elsif ($args->{cause} == 71) { + debug "[Homunculus] Call Homunculus failed because of missing item - which means we have a vaporized homunculus.\n"; + $char->{homunculus_info}{vaporized} = 1; + } + } } sub open_store_status {