1 ///////////////////////////////////////////////////////////////////////
2 //
3 // ACE - Quake II Bot Base Code
4 //
5 // Version 1.0
6 //
7 // This file is Copyright(c), Steve Yeager 1998, All Rights Reserved
8 //
9 //
10 // All other files are Copyright(c) Id Software, Inc.
11 //
12 // Please see liscense.txt in the source directory for the copyright
13 // information regarding those files belonging to Id Software, Inc.
14 //
15 // Should you decide to release a modified version of ACE, you MUST
16 // include the following text (minus the BEGIN and END lines) in the
17 // documentation for your modification.
18 //
19 // --- BEGIN ---
20 //
21 // The ACE Bot is a product of Steve Yeager, and is available from
22 // the ACE Bot homepage, at http://www.axionfx.com/ace.
23 //
24 // This program is a modification of the ACE Bot, and is therefore
25 // in NO WAY supported by Steve Yeager.
26
27 // This program MUST NOT be sold in ANY form. If you have paid for
28 // this product, you should contact Steve Yeager immediately, via
29 // the ACE Bot homepage.
30 //
31 // --- END ---
32 //
33 // I, Steve Yeager, hold no responsibility for any harm caused by the
34 // use of this source code, especially to small children and animals.
35 // It is provided as-is with no implied warranty or support.
36 //
37 // I also wish to thank and acknowledge the great work of others
38 // that has helped me to develop this code.
39 //
40 // John Cricket - For ideas and swapping code.
41 // Ryan Feltrin - For ideas and swapping code.
42 // SABIN - For showing how to do true client based movement.
43 // BotEpidemic - For keeping us up to date.
44 // Telefragged.com - For giving ACE a home.
45 // Microsoft - For giving us such a wonderful crash free OS.
46 // id - Need I say more.
47 //
48 // And to all the other testers, pathers, and players and people
49 // who I can't remember who the heck they were, but helped out.
50 //
51 ///////////////////////////////////////////////////////////////////////
52
53 ///////////////////////////////////////////////////////////////////////
54 //
55 // acebot_nodes.c - This file contains all of the
56 // pathing routines for the ACE bot.
57 //
58 ///////////////////////////////////////////////////////////////////////
59
60 #ifdef HAVE_CONFIG_H
61 #include "config.h"
62 #endif
63
64 #include "game/g_local.h"
65 #include "acebot.h"
66
67 // flags
68 qboolean newmap=true;
69
70 // Total number of nodes that are items
71 int numitemnodes;
72
73 // Total number of nodes
74 int bot_numnodes;
75
76 // For debugging paths
77 int show_path_from = -1;
78 int show_path_to = -1;
79
80 // array for node data
81 node_t nodes[MAX_NODES];
82 edict_t *node_showents[MAX_NODES];
83 short int path_table[MAX_NODES][MAX_NODES];
84
85 ///////////////////////////////////////////////////////////////////////
86 // NODE INFORMATION FUNCTIONS
87 ///////////////////////////////////////////////////////////////////////
88
89 ///////////////////////////////////////////////////////////////////////
90 // Determine cost of moving from one node to another
91 ///////////////////////////////////////////////////////////////////////
ACEND_FindCost(int from,int to)92 int ACEND_FindCost(int from, int to)
93 {
94 int curnode;
95 int cost=1; // Shortest possible is 1
96
97 // If we can not get there then return invalid
98 if (path_table[from][to] == INVALID)
99 {
100 //if(debug_mode)
101 // debug_printf("Found invalid path!");
102 return INVALID;
103 }
104 // Otherwise check the path and return the cost
105 curnode = path_table[from][to];
106
107 // Find a path (linear time, very fast)
108 while(curnode != to)
109 {
110 curnode = path_table[curnode][to];
111 if(curnode == INVALID) // something has corrupted the path abort
112 {
113 //if(debug_mode)
114 // debug_printf("Found invalid path!");
115
116 return INVALID;
117 }
118 cost++;
119 if(cost > 500) {
120 if(debug_mode)
121 debug_printf("Cost exceeded maximum!\n");
122 break;
123 }
124 }
125
126 return cost;
127 }
128
129 ///////////////////////////////////////////////////////////////////////
130 // Find the closest node to the player within a certain range
131 ///////////////////////////////////////////////////////////////////////
ACEND_FindClosestReachableNode(edict_t * self,int range,int type)132 int ACEND_FindClosestReachableNode(edict_t *self, int range, int type)
133 {
134 int i;
135 float closest = 99999;
136 float dist;
137 int node=-1;
138 vec3_t v;
139 trace_t tr;
140 float rng;
141 vec3_t maxs,mins;
142
143 VectorCopy(self->mins,mins);
144 VectorCopy(self->maxs,maxs);
145
146 // For Ladders, do not worry so much about reachability
147 if(type == NODE_LADDER)
148 {
149 VectorCopy(vec3_origin,maxs);
150 VectorCopy(vec3_origin,mins);
151 }
152 else
153 mins[2] += 18; // Stepsize
154
155 rng = (float)(range); // square range for distance comparison (eliminate sqrt)
156
157 for(i=0;i<bot_numnodes;i++)
158 {
159 if(type == NODE_ALL || type == nodes[i].type) // check node type
160 {
161 if(type == NODE_ALL && (nodes[i].type == NODE_REDBASE || nodes[i].type == NODE_BLUEBASE))
162 continue; //we don't want to look for these unless specifically doing so
163
164 VectorSubtract(nodes[i].origin, self->s.origin, v); // subtract first
165
166 dist = VectorLength(v);
167
168 if(self->current_node != -1)
169 {
170 dist = dist + abs(self->current_node - i); //try to keep the bot on the current path(i.e - nodes in sequence are weighted over those out)
171 }
172
173 if(dist < closest && dist < rng)
174 {
175 // make sure it is visible
176 tr = gi.trace (self->s.origin, mins, maxs, nodes[i].origin, self, MASK_SOLID);
177 if(tr.fraction == 1.0)
178 {
179 node = i;
180 closest = dist;
181 }
182 }
183 }
184 }
185
186 return node;
187 }
188
189 ///////////////////////////////////////////////////////////////////////
190 // BOT NAVIGATION ROUTINES
191 ///////////////////////////////////////////////////////////////////////
192
193 ///////////////////////////////////////////////////////////////////////
194 // Set up the goal
195 ///////////////////////////////////////////////////////////////////////
ACEND_SetGoal(edict_t * self,int goal_node)196 void ACEND_SetGoal(edict_t *self, int goal_node)
197 {
198 int node;
199 gitem_t *flag1_item, *flag2_item;
200 self->goal_node = goal_node;
201
202 //if flag in possession only use base nodes
203 if(ctf->value)
204 {
205 flag1_item = FindItemByClassname("item_flag_red");
206 flag2_item = FindItemByClassname("item_flag_blue");
207
208 if (self->client->pers.inventory[ITEM_INDEX(flag1_item)])
209 {
210 node = ACEND_FindClosestReachableNode(self,NODE_DENSITY*3,NODE_BLUEBASE);
211 if(node == -1)
212 node = ACEND_FindClosestReachableNode(self,NODE_DENSITY*3,NODE_ALL);
213 }
214 else if (self->client->pers.inventory[ITEM_INDEX(flag2_item)])
215 {
216 node = ACEND_FindClosestReachableNode(self,NODE_DENSITY*3,NODE_REDBASE);
217 if(node == -1)
218 node = ACEND_FindClosestReachableNode(self,NODE_DENSITY*3,NODE_ALL);
219 }
220 else
221 node = ACEND_FindClosestReachableNode(self,NODE_DENSITY*3,NODE_ALL);
222 }
223 else if(g_tactical->value) //when a base's laser barriers shut off, go into a more direct attack route
224 {
225 if (self->ctype == 1 && (!tacticalScore.alienComputer || !tacticalScore.alienPowerSource))
226 {
227 node = ACEND_FindClosestReachableNode(self,NODE_DENSITY*3,NODE_BLUEBASE);
228 if(node == -1)
229 node = ACEND_FindClosestReachableNode(self,NODE_DENSITY*3,NODE_ALL);
230 }
231 else if (self->ctype == 0 && (!tacticalScore.humanComputer || !tacticalScore.humanPowerSource))
232 {
233 node = ACEND_FindClosestReachableNode(self,NODE_DENSITY*3,NODE_REDBASE);
234 if(node == -1)
235 node = ACEND_FindClosestReachableNode(self,NODE_DENSITY*3,NODE_ALL);
236 }
237 else
238 node = ACEND_FindClosestReachableNode(self,NODE_DENSITY*3,NODE_ALL);
239 }
240 else
241 node = ACEND_FindClosestReachableNode(self, NODE_DENSITY*3, NODE_ALL);
242
243 if(node == -1)
244 return;
245
246 if(debug_mode)
247 debug_printf("%s new start node (type: %i) selected %d\n",self->client->pers.netname, nodes[node].type, node);
248
249 self->current_node = node;
250 self->next_node = self->current_node; // make sure we get to the nearest node first
251 self->node_timeout = 0;
252
253 }
254
255 ///////////////////////////////////////////////////////////////////////
256 // Move closer to goal by pointing the bot to the next node
257 // that is closer to the goal
258 ///////////////////////////////////////////////////////////////////////
ACEND_FollowPath(edict_t * self)259 qboolean ACEND_FollowPath(edict_t *self)
260 {
261 vec3_t v;
262
263 //////////////////////////////////////////
264 // Show the path
265 if(debug_mode) {
266 show_path_from = self->current_node;
267 show_path_to = self->goal_node;
268 }
269 //////////////////////////////////////////
270
271 // Try again?
272 if(self->node_timeout ++ > 30)
273 {
274 if(self->tries++ > 3)
275 return false;
276 else
277 ACEND_SetGoal(self,self->goal_node);
278 }
279
280 // Are we there yet?
281 VectorSubtract(self->s.origin,nodes[self->next_node].origin,v);
282
283 if(VectorLength(v) < 32)
284 {
285 // reset timeout
286 self->node_timeout = 0;
287
288 if(self->next_node == self->goal_node)
289 {
290 if(debug_mode)
291 debug_printf("%s reached goal!\n",self->client->pers.netname);
292
293 ACEAI_PickLongRangeGoal(self); // Pick a new goal
294 }
295 else
296 {
297 self->current_node = self->next_node;
298 self->next_node = path_table[self->current_node][self->goal_node];
299 }
300 }
301
302 if(self->current_node == -1 || self->next_node ==-1)
303 return false;
304
305 // Set bot's movement vector
306 VectorSubtract (nodes[self->next_node].origin, self->s.origin , self->move_vector);
307
308 return true;
309 }
310
311 ///////////////////////////////////////////////////////////////////////
312 // Init node array (set all to INVALID)
313 ///////////////////////////////////////////////////////////////////////
ACEND_InitNodes(void)314 void ACEND_InitNodes(void)
315 {
316 bot_numnodes = 1;
317 numitemnodes = 1;
318 memset(nodes,0,sizeof(node_t) * MAX_NODES);
319 memset(path_table,INVALID,sizeof(short int)*MAX_NODES*MAX_NODES);
320 memset(node_showents,0,sizeof(edict_t *)*MAX_NODES);
321
322 }
323
324 ///////////////////////////////////////////////////////////////////////
325 // Show the node for debugging (utility function)
326 // Previously there was a warning comment here about overflows, however it
327 // seems to be just fine on a private server.
328 ///////////////////////////////////////////////////////////////////////
ACEND_ShowNode(int node)329 void ACEND_ShowNode(int node)
330 {
331 edict_t *ent;
332
333 if (node_showents[node]) {
334 safe_bprintf(PRINT_MEDIUM, "node %d already being shown\n", node);
335 return;
336 }
337
338 ent = G_Spawn();
339
340 ent->movetype = MOVETYPE_NONE;
341 ent->solid = SOLID_NOT;
342
343 if(nodes[node].type == NODE_MOVE)
344 ent->s.renderfx = RF_SHELL_BLUE;
345 else if (nodes[node].type == NODE_WATER)
346 ent->s.renderfx = RF_SHELL_RED;
347 else
348 ent->s.renderfx = RF_SHELL_GREEN; // action nodes
349
350 ent->s.modelindex = gi.modelindex ("models/items/ammo/grenades/medium/tris.md2");
351 ent->owner = ent;
352 ent->nextthink = level.time + 200000.0;
353 ent->think = G_FreeEdict;
354 ent->dmg = 0;
355
356 VectorCopy(nodes[node].origin,ent->s.origin);
357 gi.linkentity (ent);
358
359 node_showents[node] = ent;
360
361 }
362
363 ///////////////////////////////////////////////////////////////////////
364 // Draws the current path (utility function)
365 ///////////////////////////////////////////////////////////////////////
ACEND_DrawPath()366 void ACEND_DrawPath()
367 {
368 int current_node, goal_node, next_node;
369
370 if (!debug_mode)
371 return;
372
373 current_node = show_path_from;
374 goal_node = show_path_to;
375
376 next_node = path_table[current_node][goal_node];
377
378 // Now set up and display the path
379 while(current_node != goal_node && current_node != -1)
380 {
381 gi.WriteByte (svc_temp_entity);
382 gi.WriteByte (TE_REDLASER);
383 gi.WritePosition (nodes[current_node].origin);
384 gi.WritePosition (nodes[next_node].origin);
385 gi.multicast (nodes[current_node].origin, MULTICAST_PVS);
386 current_node = next_node;
387 next_node = path_table[current_node][goal_node];
388 }
389 }
390
391 ///////////////////////////////////////////////////////////////////////
392 // Turns on showing of the path, set goal to -1 to
393 // shut off. (utility function)
394 ///////////////////////////////////////////////////////////////////////
ACEND_ShowPath(edict_t * self,int goal_node)395 void ACEND_ShowPath(edict_t *self, int goal_node)
396 {
397 show_path_from = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL);
398 show_path_to = goal_node;
399 }
400
401 ///////////////////////////////////////////////////////////////////////
402 // Add a node of type ?
403 ///////////////////////////////////////////////////////////////////////
ACEND_AddNode(edict_t * self,int type)404 int ACEND_AddNode(edict_t *self, int type)
405 {
406 vec3_t v1,v2;
407
408 // Block if we exceed maximum
409 if (bot_numnodes + 1 > MAX_NODES)
410 return false;
411
412 // Set location
413 VectorCopy(self->s.origin,nodes[bot_numnodes].origin);
414
415 // Set type
416 nodes[bot_numnodes].type = type;
417
418 /////////////////////////////////////////////////////
419 // ITEMS
420 // Move the z location up just a bit.
421 if(type == NODE_ITEM)
422 {
423 nodes[bot_numnodes].origin[2] += 16;
424 numitemnodes++;
425 }
426
427 // Teleporters
428 if(type == NODE_TELEPORTER)
429 {
430 // Up 32
431 nodes[bot_numnodes].origin[2] += 32;
432 }
433
434 if(type == NODE_LADDER)
435 {
436 nodes[bot_numnodes].type = NODE_LADDER;
437
438 if(debug_mode)
439 {
440 debug_printf("Node added %d type: Ladder\n",bot_numnodes);
441 ACEND_ShowNode(bot_numnodes);
442 }
443
444 bot_numnodes++;
445 return bot_numnodes-1; // return the node added
446
447 }
448
449 // For platforms drop two nodes one at top, one at bottom
450 if(type == NODE_PLATFORM)
451 {
452 VectorCopy(self->maxs,v1);
453 VectorCopy(self->mins,v2);
454
455 // To get the center
456 nodes[bot_numnodes].origin[0] = (v1[0] - v2[0]) / 2 + v2[0];
457 nodes[bot_numnodes].origin[1] = (v1[1] - v2[1]) / 2 + v2[1];
458 nodes[bot_numnodes].origin[2] = self->maxs[2];
459
460 if(debug_mode)
461 ACEND_ShowNode(bot_numnodes);
462
463 bot_numnodes++;
464
465 nodes[bot_numnodes].origin[0] = nodes[bot_numnodes-1].origin[0];
466 nodes[bot_numnodes].origin[1] = nodes[bot_numnodes-1].origin[1];
467 nodes[bot_numnodes].origin[2] = self->mins[2]+64;
468
469 nodes[bot_numnodes].type = NODE_PLATFORM;
470
471 // Add a link
472 ACEND_UpdateNodeEdge(bot_numnodes,bot_numnodes-1);
473
474 if(debug_mode)
475 {
476 debug_printf("Node added %d type: Platform\n",bot_numnodes);
477 ACEND_ShowNode(bot_numnodes);
478 }
479
480 bot_numnodes++;
481
482 return bot_numnodes -1;
483 }
484
485 if(debug_mode)
486 {
487 if(nodes[bot_numnodes].type == NODE_MOVE)
488 debug_printf("Node added %d type: Move\n",bot_numnodes);
489 else if(nodes[bot_numnodes].type == NODE_TELEPORTER)
490 debug_printf("Node added %d type: Teleporter\n",bot_numnodes);
491 else if(nodes[bot_numnodes].type == NODE_ITEM)
492 debug_printf("Node added %d type: Item\n",bot_numnodes);
493 else if(nodes[bot_numnodes].type == NODE_WATER)
494 debug_printf("Node added %d type: Water\n",bot_numnodes);
495 else if(nodes[bot_numnodes].type == NODE_GRAPPLE)
496 debug_printf("Node added %d type: Grapple\n",bot_numnodes);
497 else if(nodes[bot_numnodes].type == NODE_REDBASE)
498 debug_printf("Node added %d type: Red Base\n",bot_numnodes);
499 else if(nodes[bot_numnodes].type == NODE_BLUEBASE)
500 debug_printf("Node added %d type: Blue Base\n",bot_numnodes);
501 else if(nodes[bot_numnodes].type == NODE_DEFEND)
502 debug_printf("Node added %d type: Defend Base\n",bot_numnodes);
503
504 ACEND_ShowNode(bot_numnodes);
505 }
506
507 bot_numnodes++;
508
509 return bot_numnodes-1; // return the node added
510 }
511
512 ///////////////////////////////////////////////////////////////////////
513 // Add/Update node connections (paths)
514 ///////////////////////////////////////////////////////////////////////
ACEND_UpdateNodeEdge(int from,int to)515 void ACEND_UpdateNodeEdge(int from, int to)
516 {
517 int i;
518
519 if(from == -1 || to == -1 || from == to)
520 return; // safety
521
522 // Add the link
523 path_table[from][to] = to;
524
525 // Now for the self-referencing part, linear time for each link added
526 for(i=0;i<bot_numnodes;i++)
527 if(path_table[i][from] != INVALID)
528 {
529 if(i == to)
530 path_table[i][to] = INVALID; // make sure we terminate
531 else
532 path_table[i][to] = path_table[i][from];
533 }
534
535 if(debug_mode)
536 debug_printf("Link %d -> %d\n", from, to);
537 }
538
539 ///////////////////////////////////////////////////////////////////////
540 // Remove a node edge
541 ///////////////////////////////////////////////////////////////////////
ACEND_RemoveNodeEdge(edict_t * self,int from,int to)542 void ACEND_RemoveNodeEdge(edict_t *self, int from, int to)
543 {
544 int i;
545
546 if(debug_mode)
547 debug_printf("%s: Removing Edge %d -> %d\n", self->client->pers.netname, from, to);
548
549 path_table[from][to] = INVALID; // set to invalid
550
551 // Make sure this gets updated in our path array
552 for(i=0;i<bot_numnodes;i++)
553 if(path_table[from][i] == to)
554 path_table[from][i] = INVALID;
555 }
556
557 ///////////////////////////////////////////////////////////////////////
558 // This function will resolve all paths that are incomplete
559 // usually called before saving to disk
560 ///////////////////////////////////////////////////////////////////////
ACEND_ResolveAllPaths()561 void ACEND_ResolveAllPaths()
562 {
563 int i, from, to;
564 int num=0;
565
566 safe_bprintf(PRINT_HIGH,"Resolving all paths...");
567
568 for(from=0;from<bot_numnodes;from++)
569 for(to=0;to<bot_numnodes;to++)
570 {
571 // update unresolved paths
572 // Not equal to itself, not equal to -1 and equal to the last link
573 if(from != to && path_table[from][to] == to)
574 {
575 num++;
576
577 // Now for the self-referencing part linear time for each link added
578 for(i=0;i<bot_numnodes;i++)
579 if(path_table[i][from] != -1)
580 {
581 if(i == to)
582 path_table[i][to] = -1; // make sure we terminate
583 else
584 path_table[i][to] = path_table[i][from];
585 }
586 }
587 }
588
589 safe_bprintf(PRINT_MEDIUM,"done (%d updated)\n",num);
590 }
591
592 ///////////////////////////////////////////////////////////////////////
593 // Save to disk file
594 //
595 // Since my compression routines are one thing I did not want to
596 // release, I took out the compressed format option. Most levels will
597 // save out to a node file around 50-200k, so compression is not really
598 // a big deal.
599 ///////////////////////////////////////////////////////////////////////
ACEND_SaveNodes()600 void ACEND_SaveNodes()
601 {
602 FILE *pOut;
603 char filename[MAX_OSPATH];
604 char relative_path[MAX_QPATH];
605 int i,j;
606 int version = 1;
607 size_t sz;
608
609 // Resolve paths
610 ACEND_ResolveAllPaths();
611
612 safe_bprintf(PRINT_MEDIUM,"Saving node table...");
613
614 strcpy( relative_path, BOT_GAMEDATA"/nav/" );
615 strcat( relative_path, level.mapname );
616 strcat( relative_path, ".nod" );
617
618 gi.FullWritePath( filename, sizeof(filename), relative_path );
619
620 if((pOut = fopen(filename, "wb" )) == NULL)
621 {
622 gi.dprintf("ACEND_SaveNodes: failed fopen for write: %s\n", filename );
623 return;
624 }
625
626 sz = fwrite(&version,sizeof(int),1,pOut); // write version
627 sz = fwrite(&bot_numnodes,sizeof(int),1,pOut); // write count
628 sz = fwrite(&num_items,sizeof(int),1,pOut); // write facts count
629
630 sz = fwrite(nodes,sizeof(node_t),bot_numnodes,pOut); // write nodes
631
632 for(i=0;i<bot_numnodes;i++)
633 for(j=0;j<bot_numnodes;j++)
634 sz = fwrite(&path_table[i][j],sizeof(short int),1,pOut); // write count
635
636 sz = fwrite(item_table,sizeof(item_table_t),num_items,pOut); // write out the fact table
637
638 fclose(pOut);
639
640 safe_bprintf(PRINT_MEDIUM,"done.\n");
641 }
642
643 ///////////////////////////////////////////////////////////////////////
644 // Read from disk file
645 ///////////////////////////////////////////////////////////////////////
ACEND_LoadNodes(void)646 void ACEND_LoadNodes(void)
647 {
648 FILE *pIn;
649 int i,j;
650 char relative_path[MAX_QPATH];
651 char filename[MAX_OSPATH];
652 int version;
653 size_t sz;
654
655 strcpy( relative_path, BOT_GAMEDATA"/nav/" );
656 strcat( relative_path, level.mapname );
657 strcat( relative_path, ".nod" );
658
659 if ( !gi.FullPath( filename, sizeof(filename), relative_path ) )
660 {
661 gi.dprintf("ACEND_LoadNodes: not found: %s\n", relative_path );
662 }
663
664 if((pIn = fopen(filename, "rb" )) == NULL)
665 {
666 // Create item table
667 gi.dprintf("ACE: No node file found, creating new one...");
668 ACEIT_BuildItemNodeTable(false);
669 safe_bprintf(PRINT_MEDIUM, "done.\n");
670 return;
671 }
672
673 // determine version
674 sz = fread(&version,sizeof(int),1,pIn); // read version
675
676 if(version == 1)
677 {
678 gi.dprintf("ACE: Loading node table...");
679
680 //FIXME: This code sucks so bad. Didn't anyone ever tell this Yeager
681 //fellow that dumps of C structs don't make a proper file format?
682 //If your compiler does padding differently, it breaks. If you want to
683 //add new field types, it breaks.
684 sz = fread(&bot_numnodes,sizeof(int),1,pIn); // read count
685 sz = fread(&num_items,sizeof(int),1,pIn); // read facts count
686 sz = fread(nodes,sizeof(node_t),bot_numnodes,pIn);
687
688 for(i=0;i<bot_numnodes;i++)
689 for(j=0;j<bot_numnodes;j++)
690 sz = fread(&path_table[i][j],sizeof(short int),1,pIn); // write count
691
692 for(i=0;i<num_items;i++)
693 sz = fread(item_table,sizeof(item_table_t),1,pIn);
694 fclose(pIn);
695 }
696 else
697 {
698 // Create item table
699 gi.dprintf("ACE: No node file found, creating new one...");
700 ACEIT_BuildItemNodeTable(false);
701 safe_bprintf(PRINT_MEDIUM, "done.\n");
702 return; // bail
703 }
704
705 gi.dprintf("done.\n");
706
707 ACEIT_BuildItemNodeTable(true);
708
709 }
710
711