MapRoute.pm
上传用户:market2
上传日期:2018-11-18
资源大小:18786k
文件大小:16k
源码类别:

外挂编程

开发平台:

Windows_Unix

  1. #########################################################################
  2. #  OpenKore - Inter-map movement task
  3. #  Copyright (c) 2006 OpenKore Team
  4. #
  5. #  This software is open source, licensed under the GNU General Public
  6. #  License, version 2.
  7. #  Basically, this means that you're allowed to modify and distribute
  8. #  this software. However, if you distribute modified versions, you MUST
  9. #  also distribute the source code.
  10. #  See http://www.gnu.org/licenses/gpl.html for the full license.
  11. #########################################################################
  12. # The MapRoute task is like the Route task, but allows you to walk routes
  13. # that span different maps.
  14. package Task::MapRoute;
  15. use strict;
  16. use Time::HiRes qw(time);
  17. use Scalar::Util;
  18. use Modules 'register';
  19. use Globals;
  20. use Task::WithSubtask;
  21. use Task::Route;
  22. use Task::CalcMapRoute;
  23. use Task::TalkNPC;
  24. use base qw(Task::WithSubtask);
  25. use Translation qw(T TF);
  26. use Log qw(message debug warning error);
  27. use Network;
  28. use Plugins;
  29. use Misc qw(useTeleport);
  30. use Utils qw(timeOut distance existsInList);
  31. use Utils::PathFinding;
  32. use Utils::Exceptions;
  33. # Error constants.
  34. use enum (
  35. # Routing errors
  36. qw(TOO_MUCH_TIME
  37. CANNOT_CALCULATE_ROUTE
  38. STUCK
  39. CANNOT_LOAD_FIELD),
  40. # NPC errors
  41. qw(NPC_NOT_FOUND
  42. NPC_NO_RESPONSE
  43. NO_SHOP_ITEM
  44. WRONG_NPC_INSTRUCTIONS),
  45. qw(UNKNOWN_ERROR)
  46. );
  47. # TODO: this task should lock the 'npc' mutex when talking to NPCs!
  48. ##
  49. # Task::MapRoute->new(options...)
  50. #
  51. # Create a new Task::Route object. The following options are allowed:
  52. # `l
  53. # - All options allowed by Task::WithSubtask->new(), except 'mutexes', 'autostop' and 'autofail'.
  54. # - map (required) - The map you want to go to, for example "prontera".
  55. # - x, y - The coordinate on the destination map you want to walk to. On some maps this is
  56. #          important because they're split by a river. Depending on which side of the river
  57. #          you want to be, the route may be different.
  58. # - maxDistance - The maximum distance (in blocks) that the route may be. If
  59. #                 not specified, then there is no limit.
  60. # - maxTime - The maximum time that may be spent on walking the route. If not
  61. #             specified, then there is no time limit.
  62. # - distFromGoal - Stop walking if we're within the specified distance (in blocks)
  63. #                  from the goal. If not specified, then we'll walk until the
  64. #                  destination is reached.
  65. # - pyDistFromGoal - Same as distFromGoal, but this allows you to specify the
  66. #                    Pythagorian distance instead of block distance.
  67. # - avoidWalls - Whether to avoid walls. The default is true.
  68. # - notifyUponArrival - Whether to print a message when we've reached the destination.
  69. #                       The default is no.
  70. # `l`
  71. sub new {
  72. my $class = shift;
  73. my %args = @_;
  74. # TODO: do we need a mutex 'npc' too?
  75. my $self = $class->SUPER::new(@_, autostop => 1, autofail => 0, mutexes => ['movement']);
  76. if (!$args{map}) {
  77. ArgumentException->throw(error => "Invalid arguments.");
  78. }
  79. my $allowed = new Set('maxDistance', 'maxTime', 'distFromGoal', 'pyDistFromGoal',
  80. 'avoidWalls', 'notifyUponArrival');
  81. foreach my $key (keys %args) {
  82. if ($allowed->has($key) && defined $args{$key}) {
  83. $self->{$key} = $args{$key};
  84. }
  85. }
  86. $self->{dest}{map} = $args{map};
  87. $self->{dest}{pos}{x} = $args{x};
  88. $self->{dest}{pos}{y} = $args{y};
  89. if ($config{'route_avoidWalls'}) {
  90. $self->{avoidWalls} = 1 if (!defined $self->{avoidWalls});
  91. } else {$self->{avoidWalls} = 0;}
  92. # Watch for map change events. Pass a weak reference to ourselves in order
  93. # to avoid circular references (memory leaks).
  94. my @holder = ($self);
  95. Scalar::Util::weaken($holder[0]);
  96. $self->{mapChangedHook} = Plugins::addHook('Network::Receive::map_changed', &mapChanged, @holder);
  97. return $self;
  98. }
  99. sub DESTROY {
  100. my ($self) = @_;
  101. Plugins::delHook($self->{mapChangedHook});
  102. $self->SUPER::DESTROY();
  103. }
  104. sub activate {
  105. my ($self) = @_;
  106. $self->SUPER::activate();
  107. $self->initMapCalculator() if ($net->getState() == Network::IN_GAME && $field);
  108. $self->{time_start} = time;
  109. }
  110. sub iterate {
  111. my ($self) = @_;
  112. return if (!$self->SUPER::iterate() || $net->getState() != Network::IN_GAME);
  113. return if (!$field || !defined $char->{pos_to}{x} || !defined $char->{pos_to}{y});
  114. # When the CalcMapRouter subtask finishes, a new Route task may be set as subtask.
  115. # In that case we don't want to continue or this MapRoute task may end prematurely.
  116. #
  117. # If the Route task bails out with an error then our subtaskDone() method will set
  118. # an error in this task too. In that case we don't want to continue.
  119. return if ($self->getSubtask() || $self->getStatus() != Task::RUNNING);
  120. my @solution;
  121. if (!$self->{mapSolution}) {
  122. $self->initMapCalculator();
  123. } elsif (@{$self->{mapSolution}} == 0) {
  124. $self->setDone();
  125. debug "Map Router has finished traversing the map solutionn", "route";
  126. } elsif ( $field->name() ne $self->{mapSolution}[0]{map}
  127.      || ( $self->{mapChanged} && !$self->{teleport} ) ) {
  128. # Solution Map does not match current map
  129. debug "Current map " . $field->name() . " does not match solution [ $self->{mapSolution}[0]{portal} ].n", "route";
  130. delete $self->{substage};
  131. delete $self->{timeout};
  132. delete $self->{mapChanged};
  133. shift @{$self->{mapSolution}};
  134. } elsif ( $self->{mapSolution}[0]{steps} ) {
  135. # If current solution has conversation steps specified
  136. if ( $self->{substage} eq 'Waiting for Warp' ) {
  137. $self->{timeout} = time unless $self->{timeout};
  138. if (timeOut($self->{timeout}, $timeout{ai_route_npcTalk}{timeout} || 10)
  139.  || $ai_v{npc_talk}{talk} eq 'close') {
  140. # We waited for 10 seconds and got nothing
  141. delete $self->{substage};
  142. delete $self->{timeout};
  143. if (++$self->{mapSolution}[0]{retry} >= ($config{route_maxNpcTries} || 5)) {
  144. # NPC sequence is a failure
  145. # We delete that portal and try again
  146. delete $portals_lut{"$self->{mapSolution}[0]{map} $self->{mapSolution}[0]{pos}{x} $self->{mapSolution}[0]{pos}{y}"};
  147. warning TF("Unable to talk to NPC at %s (%s,%s).n", $field->name(), $self->{mapSolution}[0]{pos}{x}, $self->{mapSolution}[0]{pos}{y}), "route";
  148. $self->initMapCalculator(); # redo MAP router
  149. }
  150. }
  151. } elsif (distance($char->{pos_to}, $self->{mapSolution}[0]{pos}) <= 10) {
  152. my ($from,$to) = split /=/, $self->{mapSolution}[0]{portal};
  153. if ($char->{zenny} >= $portals_lut{$from}{dest}{$to}{cost}) {
  154. # We have enough money for this service.
  155. $self->{substage} = 'Waiting for Warp';
  156. $self->{old_x} = $char->{pos_to}{x};
  157. $self->{old_y} = $char->{pos_to}{y};
  158. $self->{old_map} = $field->name();
  159. my $task = new Task::TalkNPC(
  160. x => $self->{mapSolution}[0]{pos}{x},
  161. y => $self->{mapSolution}[0]{pos}{y},
  162. sequence => $self->{mapSolution}[0]{steps});
  163. $self->setSubtask($task);
  164. } else {
  165. error TF("Insufficient zenny to pay for service at %s (%s,%s).n",
  166. $field->name(), $self->{mapSolution}[0]{pos}{x},
  167. $self->{mapSolution}[0]{pos}{y}), "route";
  168. $self->initMapCalculator(); # Redo MAP router
  169. }
  170. } elsif ( $self->{maxTime} && time - $self->{time_start} > $self->{maxTime} ) {
  171. # We spent too long a time.
  172. debug "MapRoute - We spent too much time; bailing out.n", "route";
  173. $self->setError(TOO_MUCH_TIME, "Too much time spent on route traversal.");
  174. } elsif ( Task::Route->getRoute(@solution, $field, $char->{pos_to}, $self->{mapSolution}[0]{pos}) ) {
  175. # NPC is reachable from current position
  176. # >> Then "route" to it
  177. debug "Walking towards the NPCn", "route";
  178. my $task = new Task::Route(
  179. x => $self->{mapSolution}[0]{pos}{x},
  180. y => $self->{mapSolution}[0]{pos}{y},
  181. maxTime => $self->{maxTime},
  182. distFromGoal => 10,
  183. avoidWalls => $self->{avoidWalls},
  184. solution => @solution
  185. );
  186. $self->setSubtask($task);
  187. } else {
  188. # Error, NPC is not reachable from current pos
  189. debug "CRITICAL ERROR: NPC is not reachable from current location.n", "route";
  190. error TF("Unable to walk from %s (%s,%s) to NPC at (%s,%s).n", $field->name(), $char->{pos_to}{x}, $char->{pos_to}{y}, $self->{mapSolution}[0]{pos}{x}, $self->{mapSolution}[0]{pos}{y}), "route";
  191. shift @{$self->{mapSolution}};
  192. }
  193. } elsif ( $self->{mapSolution}[0]{portal} eq "$self->{mapSolution}[0]{map} $self->{mapSolution}[0]{pos}{x} $self->{mapSolution}[0]{pos}{y}=$self->{mapSolution}[0]{map} $self->{mapSolution}[0]{pos}{x} $self->{mapSolution}[0]{pos}{y}" ) {
  194. # This solution points to an X,Y coordinate
  195. my $distFromGoal = $self->{pyDistFromGoal}
  196. ? $self->{pyDistFromGoal}
  197. : ($self->{distFromGoal} ? $self->{distFromGoal} : 0);
  198. if ( $distFromGoal + 2 > distance($char->{pos_to}, $self->{mapSolution}[0]{pos})) {
  199. # We need to specify +2 because sometimes the exact spot is occupied by someone else
  200. shift @{$self->{mapSolution}};
  201. } elsif ( $self->{maxTime} && time - $self->{time_start} > $self->{maxTime} ) {
  202. # We spent too long a time.
  203. debug "We spent too much time; bailing out.n", "route";
  204. $self->setError(TOO_MUCH_TIME, "Too much time spent on route traversal.");
  205. } elsif ( Task::Route->getRoute(@solution, $field, $char->{pos_to}, $self->{mapSolution}[0]{pos}) ) {
  206. # X,Y is reachable from current position
  207. # >> Then "route" to it
  208. my $task = new Task::Route(
  209. x => $self->{mapSolution}[0]{pos}{x},
  210. y => $self->{mapSolution}[0]{pos}{y},
  211. maxTime => $self->{maxTime},
  212. avoidWalls => $self->{avoidWalls},
  213. distFromGoal => $self->{distFromGoal},
  214. pyDistFromGoal => $self->{pyDistFromGoal},
  215. solution => @solution
  216. );
  217. $self->setSubtask($task);
  218. } else {
  219. warning TF("No LOS from %s (%s,%s) to Final Destination at (%s,%s).n",
  220. $field->name(), $char->{pos_to}{x}, $char->{pos_to}{y},
  221. $self->{mapSolution}[0]{pos}{x},
  222. $self->{mapSolution}[0]{pos}{y}), "route";
  223. error TF("Cannot reach (%s,%s) from current position.n",
  224. $self->{mapSolution}[0]{pos}{x},
  225. $self->{mapSolution}[0]{pos}{y}), "route";
  226. shift @{$self->{mapSolution}};
  227. }
  228. } elsif ( $portals_lut{"$self->{mapSolution}[0]{map} $self->{mapSolution}[0]{pos}{x} $self->{mapSolution}[0]{pos}{y}"}{source} ) {
  229. # This is a portal solution
  230. if ( distance($char->{pos_to}, $self->{mapSolution}[0]{pos}) < 2 ) {
  231. # Portal is within 'Enter Distance'
  232. $timeout{ai_portal_wait}{timeout} = $timeout{ai_portal_wait}{timeout} || 0.5;
  233. if ( timeOut($timeout{ai_portal_wait}) ) {
  234. $messageSender->sendMove(int($self->{mapSolution}[0]{'pos'}{'x'}), int($self->{mapSolution}[0]{'pos'}{'y'}) );
  235. $timeout{'ai_portal_wait'}{'time'} = time;
  236. }
  237. } else {
  238. my $walk = 1;
  239. # Teleport until we're close enough to the portal
  240. $self->{teleport} = $config{route_teleport} if (!defined $self->{teleport});
  241. if ($self->{teleport} && !$cities_lut{$field->name() . ".rsw"}
  242. && !existsInList($config{route_teleport_notInMaps}, $field->name())
  243. && ( !$config{route_teleport_maxTries} || $self->{teleportTries} <= $config{route_teleport_maxTries} )) {
  244. my $minDist = $config{route_teleport_minDistance};
  245. if ($self->{mapChanged}) {
  246. undef $self->{sentTeleport};
  247. undef $self->{mapChanged};
  248. }
  249. if (!$self->{sentTeleport}) {
  250. # Find first inter-map portal
  251. my $portal;
  252. for my $x (@{$self->{mapSolution}}) {
  253. $portal = $x;
  254. last unless $x->{map} eq $x->{dest_map};
  255. }
  256. my $dist = new PathFinding(
  257. start => $char->{pos_to},
  258. dest => $portal->{pos},
  259. field => $field
  260. )->runcount;
  261. debug "Distance to portal ($portal->{portal}) is $distn", "route_teleport";
  262. if ($dist <= 0 || $dist > $minDist) {
  263. if ($dist > 0 && $config{route_teleport_maxTries} && $self->{teleportTries} >= $config{route_teleport_maxTries}) {
  264. debug "Teleported $config{route_teleport_maxTries} times. Falling back to walking.n", "route_teleport";
  265. } else {
  266. message TF("Attempting to teleport near portal, try #%sn", ($self->{teleportTries} + 1)), "route_teleport";
  267. if (!useTeleport(1)) {
  268. $self->{teleport} = 0;
  269. } else {
  270. $walk = 0;
  271. $self->{sentTeleport} = 1;
  272. $self->{teleportTime} = time;
  273. $self->{teleportTries}++;
  274. }
  275. }
  276. }
  277. } elsif (timeOut($self->{teleportTime}, 4)) {
  278. debug "Unable to teleport; falling back to walking.n", "route_teleport";
  279. $self->{teleport} = 0;
  280. } else {
  281. $walk = 0;
  282. }
  283. }
  284. if ($walk) {
  285. if ( Task::Route->getRoute( @solution, $field, $char->{pos_to}, $self->{mapSolution}[0]{pos} ) ) {
  286. # Portal is reachable from current position
  287. # >> Then "route" to it
  288. debug "Portal route within same map.n", "route";
  289. $self->{teleportTries} = 0;
  290. my $task = new Task::Route(
  291. x => $self->{mapSolution}[0]{pos}{x},
  292. y => $self->{mapSolution}[0]{pos}{y},
  293. maxTime => $self->{maxTime},
  294. avoidWalls => $self->{avoidWalls},
  295. solution => @solution
  296. );
  297. $self->setSubtask($task);
  298. } else {
  299. warning TF("No LOS from %s (%s,%s) to Portal at (%s,%s).n",
  300. $field->name(), $char->{pos_to}{x}, $char->{pos_to}{y},
  301. $self->{mapSolution}[0]{pos}{x}, $self->{mapSolution}[0]{pos}{y}),
  302. "route";
  303. error T("Cannot reach portal from current positionn"), "route";
  304. shift @{$self->{mapSolution}};
  305. }
  306. }
  307. }
  308. }
  309. }
  310. sub initMapCalculator {
  311. my ($self) = @_;
  312. my $task = new Task::CalcMapRoute(
  313. sourceMap => $field->name(),
  314. sourceX => $char->{pos_to}{x},
  315. sourceY => $char->{pos_to}{y},
  316. map => $self->{dest}{map},
  317. x => $self->{dest}{pos}{x},
  318. y => $self->{dest}{pos}{y}
  319. );
  320. $self->setSubtask($task);
  321. }
  322. sub subtaskDone {
  323. my ($self, $task) = @_;
  324. if ($task->isa('Task::CalcMapRoute')) {
  325. my $error = $task->getError();
  326. if ($error) {
  327. my $code;
  328. if ($error->{code} == Task::CalcMapRoute::CANNOT_LOAD_FIELD) {
  329. $code = CANNOT_LOAD_FIELD;
  330. } elsif ($error->{code} == Task::CalcMapRoute::CANNOT_CALCULATE_ROUTE) {
  331. $code = CANNOT_CALCULATE_ROUTE;
  332. }
  333. $self->setError($code, $error->{message});
  334. } else {
  335. $self->{mapSolution} = $task->getRoute();
  336. # The map solution is empty, meaning that the destination
  337. # is on the same map and that we can walk there directly.
  338. # Of course, we only do that if we have a specific position
  339. # to walk to.
  340. if (@{$self->{mapSolution}} == 0 && defined($self->{dest}{pos}{x}) && defined($self->{dest}{pos}{y})) {
  341. my $task = new Task::Route(
  342. x => $self->{dest}{pos}{x},
  343. y => $self->{dest}{pos}{y},
  344. maxTime => $self->{maxTime},
  345. avoidWalls => $self->{avoidWalls},
  346. distFromGoal => $self->{distFromGoal},
  347. pyDistFromGoal => $self->{pyDistFromGoal}
  348. );
  349. $self->setSubtask($task);
  350. }
  351. }
  352. } elsif ($task->isa('Task::Route')) {
  353. my $error = $task->getError();
  354. if ($error) {
  355. my $code;
  356. if ($error->{code} == Task::Route::TOO_MUCH_TIME) {
  357. $code = TOO_MUCH_TIME;
  358. } elsif ($error->{code} == Task::Route::CANNOT_CALCULATE_ROUTE) {
  359. $code = CANNOT_CALCULATE_ROUTE;
  360. } elsif ($error->{code} == Task::Route::STUCK) {
  361. $code = STUCK;
  362. } else {
  363. $code = UNKNOWN_ERROR;
  364. }
  365. $self->setError($code, $error->{message});
  366. }
  367. } elsif ($task->isa('Task::TalkNPC')) {
  368. my $error = $task->getError();
  369. if ($error) {
  370. my $code;
  371. if ($error->{code} == Task::TalkNPC::NPC_NOT_FOUND) {
  372. $code = NPC_NOT_FOUND;
  373. } elsif ($error->{code} == Task::TalkNPC::NPC_NO_RESPONSE) {
  374. $code = NPC_NO_RESPONSE;
  375. } elsif ($error->{code} == Task::TalkNPC::NO_SHOP_ITEM) {
  376. $code = NO_SHOP_ITEM;
  377. } elsif ($error->{code} == Task::TalkNPC::WRONG_NPC_INSTRUCTIONS) {
  378. $code = WRONG_NPC_INSTRUCTIONS;
  379. } else {
  380. $code = UNKNOWN_ERROR;
  381. }
  382. $self->setError($code, $error->{message});
  383. }
  384. } elsif (my $error = $task->getError()) {
  385. $self->setError(UNKNOWN_ERROR, $error->{message});
  386. }
  387. }
  388. sub mapChanged {
  389. my (undef, undef, $holder) = @_;
  390. my $self = $holder->[0];
  391. $self->{mapChanged} = 1;
  392. }
  393. 1;