1 /*
2 * Created by Ian "Goober5000" Warfield for the FreeSpace2 Source Code Project.
3 * You may not sell or otherwise commercially exploit the source or things you
4 * create based on the source.
5 */
6
7
8
9 #include "globalincs/def_files.h"
10 #include "iff_defs/iff_defs.h"
11 #include "parse/parselo.h"
12 #include "hud/hud.h"
13 #include "mission/missionparse.h"
14 #include "ship/ship.h"
15 #include "io/timer.h"
16
17 extern int radar_target_id_flags;
18
19 int Num_iffs;
20 iff_info Iff_info[MAX_IFFS];
21
22 int Iff_traitor;
23
24 int radar_iff_color[5][2][4];
25 int iff_bright_delta;
26 int *iff_color_brightness = &iff_bright_delta;
27
28 // global only to file
29 color Iff_colors[MAX_IFF_COLORS][2]; // AL 1-2-97: Create two IFF colors, regular and bright
30
31 flag_def_list rti_flags[] = {
32 { "crosshairs", RTIF_CROSSHAIRS, 0 },
33 { "blink", RTIF_BLINK, 0 },
34 { "pulsate", RTIF_PULSATE, 0 },
35 { "enlarge", RTIF_ENLARGE, 0 }
36 };
37
38 int Num_rti_flags = sizeof(rti_flags)/sizeof(flag_def_list);
39
40 /**
41 * Borrowed from ship.cpp, ship_iff_init_colors
42 *
43 * @param is_bright Whether set to bright
44 */
iff_get_alpha_value(bool is_bright)45 int iff_get_alpha_value(bool is_bright)
46 {
47 if (is_bright == false)
48 return (HUD_COLOR_ALPHA_MAX - iff_bright_delta) * 16;
49 else
50 return HUD_COLOR_ALPHA_MAX * 16;
51 }
52
53 /**
54 * Init a color and add it to the ::Iff_colors array
55 *
56 * @param r Red
57 * @param g Green
58 * @param b Blue
59 *
60 * @return The new IFF colour slot in ::Iff_colors array
61 */
iff_init_color(int r,int g,int b)62 int iff_init_color(int r, int g, int b)
63 {
64 typedef struct temp_color_t {
65 int r;
66 int g;
67 int b;
68 } temp_color_t;
69
70 int i, idx;
71 temp_color_t *c;
72
73 static int num_iff_colors = 0;
74 static temp_color_t temp_colors[MAX_IFF_COLORS];
75
76 Assert(r >= 0 && r <= 255);
77 Assert(g >= 0 && g <= 255);
78 Assert(b >= 0 && b <= 255);
79
80 // make sure we're under the limit
81 if (num_iff_colors >= MAX_IFF_COLORS)
82 {
83 Warning(LOCATION, "Too many iff colors! Ignoring the rest...\n");
84 return 0;
85 }
86
87 // find out if this color is in use
88 for (i = 0; i < num_iff_colors; i++)
89 {
90 c = &temp_colors[i];
91
92 if (c->r == r && c->g == g && c->b == b)
93 return i;
94 }
95
96 // not in use, so add a new slot
97 idx = num_iff_colors;
98 num_iff_colors++;
99
100 // save the values
101 c = &temp_colors[idx];
102 c->r = r;
103 c->g = g;
104 c->b = b;
105
106 // init it
107 gr_init_alphacolor(&Iff_colors[idx][0], r, g, b, iff_get_alpha_value(false));
108 gr_init_alphacolor(&Iff_colors[idx][1], r, g, b, iff_get_alpha_value(true));
109
110 // return the new slot
111 return idx;
112 }
113
114 /**
115 * Parse the table
116 */
iff_init()117 void iff_init()
118 {
119 char traitor_name[NAME_LENGTH];
120 char attack_names[MAX_IFFS][MAX_IFFS][NAME_LENGTH];
121 struct {
122 char iff_name[NAME_LENGTH];
123 int color_index;
124 } observed_color_table[MAX_IFFS][MAX_IFFS];
125
126 int num_attack_names[MAX_IFFS];
127 int num_observed_colors[MAX_IFFS];
128 int i, j, k;
129 int string_idx;
130
131 int rval;
132 if ((rval = setjmp(parse_abort)) != 0)
133 {
134 mprintf(("TABLES: Unable to parse '%s'! Error code = %i.\n", "iff_defs.tbl", rval));
135 return;
136 }
137
138 // Goober5000 - if table doesn't exist, use the default table
139 if (cf_exists_full("iff_defs.tbl", CF_TYPE_TABLES))
140 read_file_text("iff_defs.tbl", CF_TYPE_TABLES);
141 else
142 read_file_text_from_array(defaults_get_file("iff_defs.tbl"));
143
144 reset_parse();
145
146 // parse the table --------------------------------------------------------
147
148 required_string("#IFFs");
149
150 // get the traitor
151 required_string("$Traitor IFF:");
152 stuff_string(traitor_name, F_NAME, NAME_LENGTH);
153
154 int rgb[3];
155
156 // check if alternate colours are wanted to be used for these
157 // Marks various stuff... like asteroids
158 if ((optional_string("$Selection Color:")) || (optional_string("$Selection Colour:")))
159 {
160 stuff_int_list(rgb, 3, RAW_INTEGER_TYPE);
161 iff_init_color(rgb[0], rgb[1], rgb[2]);
162 }
163 else
164 iff_init_color(0xff, 0xff, 0xff);
165
166 // Marks the ship currently saying something
167 if ((optional_string("$Message Color:")) || (optional_string("$Message Colour:")))
168 {
169 stuff_int_list(rgb, 3, RAW_INTEGER_TYPE);
170 iff_init_color(rgb[0], rgb[1], rgb[2]);
171 }
172 else
173 iff_init_color(0x7f, 0x7f, 0x7f);
174
175 // Marks the tagged ships
176 if ((optional_string("$Tagged Color:")) || (optional_string("$Tagged Colour:")))
177 {
178 stuff_int_list(rgb, 3, RAW_INTEGER_TYPE);
179 iff_init_color(rgb[0], rgb[1], rgb[2]);
180 }
181 else
182 iff_init_color(0xff, 0xff, 0x00);
183
184 // init radar blips colour table
185 int a_bright,a_dim;
186 bool alternate_blip_color = false;
187 for (i=0;i<5;i++)
188 {
189 for (j=0;j<2;j++)
190 {
191 for (k=0;k<3;k++)
192 {
193 radar_iff_color[i][j][k] = -1;
194 }
195 }
196 }
197
198 // if the bright/dim scaling is wanted to be changed
199 if (optional_string("$Dimmed IFF brightness:"))
200 {
201 int dim_iff_brightness;
202 stuff_int(&dim_iff_brightness);
203 Assert(dim_iff_brightness >= 0 && dim_iff_brightness <= HUD_COLOR_ALPHA_MAX);
204 *iff_color_brightness = dim_iff_brightness;
205 }
206 else
207 *iff_color_brightness = 4;
208
209 // alternate = use same method as with ship blips
210 // retail = use 1/2 intensities
211 if (optional_string("$Use Alternate Blip Coloring:") || optional_string("$Use Alternate Blip Colouring:"))
212 {
213 stuff_boolean(&alternate_blip_color);
214 }
215
216 // Parse blip colours, their order is hardcoded.
217 if ((optional_string("$Missile Blip Color:")) || (optional_string("$Missile Blip Colour:")))
218 {
219 stuff_int_list(rgb, 3, RAW_INTEGER_TYPE);
220 for (i=0;i<3;i++)
221 {
222 Assert(rgb[i] >= 0 && rgb[i] <= 255);
223 radar_iff_color[0][1][i] = rgb[i];
224 radar_iff_color[0][0][i] = rgb[i]/2;
225 }
226 }
227
228 if ((optional_string("$Navbuoy Blip Color:")) || (optional_string("$Navbuoy Blip Colour:")))
229 {
230 stuff_int_list(rgb, 3, RAW_INTEGER_TYPE);
231 for (i=0;i<3;i++)
232 {
233 Assert(rgb[i] >= 0 && rgb[i] <= 255);
234 radar_iff_color[1][1][i] = rgb[i];
235 radar_iff_color[1][0][i] = rgb[i]/2;
236 }
237 }
238
239 if ((optional_string("$Warping Blip Color:")) || (optional_string("$Warping Blip Colour:")))
240 {
241 stuff_int_list(rgb, 3, RAW_INTEGER_TYPE);
242 for (i=0;i<3;i++)
243 {
244 Assert(rgb[i] >= 0 && rgb[i] <= 255);
245 radar_iff_color[2][1][i] = rgb[i];
246 radar_iff_color[2][0][i] = rgb[i]/2;
247 }
248 }
249
250 if ((optional_string("$Node Blip Color:")) || (optional_string("$Node Blip Colour:")))
251 {
252 stuff_int_list(rgb, 3, RAW_INTEGER_TYPE);
253 for (i=0;i<3;i++)
254 {
255 Assert(rgb[i] >= 0 && rgb[i] <= 255);
256 radar_iff_color[3][1][i] = rgb[i];
257 radar_iff_color[3][0][i] = rgb[i]/2;
258 }
259 }
260
261 if ((optional_string("$Tagged Blip Color:")) || (optional_string("$Tagged Blip Colour:")))
262 {
263 stuff_int_list(rgb, 3, RAW_INTEGER_TYPE);
264 for (i=0;i<3;i++)
265 {
266 Assert(rgb[i] >= 0 && rgb[i] <= 255);
267 radar_iff_color[4][1][i] = rgb[i];
268 radar_iff_color[4][0][i] = rgb[i]/2;
269 }
270 }
271
272 if (alternate_blip_color == true)
273 {
274 a_bright = iff_get_alpha_value(true);
275 a_dim = iff_get_alpha_value(false);
276 for (i=0;i<5;i++)
277 {
278 if (radar_iff_color[i][0][0] >= 0)
279 {
280 for (j=0;j<3;j++)
281 {
282 radar_iff_color[i][0][j] = radar_iff_color[i][1][j];
283 }
284
285 radar_iff_color[i][1][3] = a_bright;
286 radar_iff_color[i][0][3] = a_dim;
287 }
288 }
289 }
290 else
291 {
292 for (i=0;i<5;i++)
293 {
294 if (radar_iff_color[i][0][0] >= 0)
295 {
296 radar_iff_color[i][0][3] = 255;
297 radar_iff_color[i][1][3] = 255;
298 }
299 }
300 }
301
302 if (optional_string("$Radar Target ID Flags:")) {
303 parse_string_flag_list((int*)&radar_target_id_flags, rti_flags, Num_rti_flags);
304 if (optional_string("+reset"))
305 radar_target_id_flags = 0;
306 }
307
308 // begin reading data
309 Num_iffs = 0;
310 while (required_string_either("#End", "$IFF Name:"))
311 {
312 iff_info *iff;
313 int cur_iff;
314
315 // make sure we're under the limit
316 if (Num_iffs >= MAX_IFFS)
317 {
318 Warning(LOCATION, "Too many iffs in iffs_defs.tbl! Max is %d.\n", MAX_IFFS);
319 skip_to_start_of_string("#End", NULL);
320 break;
321 }
322
323 // add new IFF
324 iff = &Iff_info[Num_iffs];
325 cur_iff = Num_iffs;
326 Num_iffs++;
327
328
329 // get required IFF info ----------------------------------------------
330
331 // get the iff name
332 required_string("$IFF Name:");
333 stuff_string(iff->iff_name, F_NAME, NAME_LENGTH);
334
335 // get the iff color
336 if (check_for_string("$Colour:"))
337 required_string("$Colour:");
338 else
339 required_string("$Color:");
340 stuff_int_list(rgb, 3, RAW_INTEGER_TYPE);
341 iff->color_index = iff_init_color(rgb[0], rgb[1], rgb[2]);
342
343
344 // get relationships between IFFs -------------------------------------
345
346 // get the list of iffs attacked
347 if (optional_string("$Attacks:"))
348 num_attack_names[cur_iff] = stuff_string_list(attack_names[cur_iff], MAX_IFFS);
349 else
350 num_attack_names[cur_iff] = 0;
351
352 // get the list of observed colors
353 num_observed_colors[cur_iff] = 0;
354 while (optional_string("+Sees"))
355 {
356 // get iff observed
357 stuff_string_until(observed_color_table[cur_iff][num_observed_colors[cur_iff]].iff_name, "As:", NAME_LENGTH);
358 required_string("As:");
359
360 // get color observed
361 stuff_int_list(rgb, 3, RAW_INTEGER_TYPE);
362 observed_color_table[cur_iff][num_observed_colors[cur_iff]].color_index = iff_init_color(rgb[0], rgb[1], rgb[2]);
363
364 // increment
365 num_observed_colors[cur_iff]++;
366 }
367
368
369 // get flags ----------------------------------------------------------
370
371 // get iff flags
372 iff->flags = 0;
373 if (optional_string("$Flags:"))
374 {
375 char flag_strings[MAX_IFF_FLAGS][NAME_LENGTH];
376
377 int num_strings = stuff_string_list(flag_strings, MAX_IFF_FLAGS);
378 for (string_idx = 0; string_idx < num_strings; string_idx++)
379 {
380 if (!stricmp(NOX("support allowed"), flag_strings[string_idx]))
381 iff->flags |= IFFF_SUPPORT_ALLOWED;
382 else if (!stricmp(NOX("exempt from all teams at war"), flag_strings[string_idx]))
383 iff->flags |= IFFF_EXEMPT_FROM_ALL_TEAMS_AT_WAR;
384 else if (!stricmp(NOX("orders hidden"), flag_strings[string_idx]))
385 iff->flags |= IFFF_ORDERS_HIDDEN;
386 else if (!stricmp(NOX("orders shown"), flag_strings[string_idx]))
387 iff->flags |= IFFF_ORDERS_SHOWN;
388 else if (!stricmp(NOX("wing name hidden"), flag_strings[string_idx]))
389 iff->flags |= IFFF_WING_NAME_HIDDEN;
390 else
391 Warning(LOCATION, "Bogus string in iff flags: %s\n", flag_strings[string_idx]);
392 }
393 }
394
395 // get default ship flags
396 iff->default_parse_flags = 0;
397 if (optional_string("$Default Ship Flags:"))
398 {
399 i = 0;
400 j = 0;
401 char flag_strings[MAX_PARSE_OBJECT_FLAGS][NAME_LENGTH];
402 int num_strings = stuff_string_list(flag_strings, MAX_PARSE_OBJECT_FLAGS);
403 for (i = 0; i < num_strings; i++)
404 {
405 for (j = 0; j < MAX_PARSE_OBJECT_FLAGS; j++)
406 {
407 if (!stricmp(flag_strings[i], Parse_object_flags[j]))
408 {
409 iff->default_parse_flags |= (1 << j);
410 break;
411 }
412 }
413 }
414
415 if (j == MAX_PARSE_OBJECT_FLAGS)
416 Warning(LOCATION, "Bogus string in iff default ship flags: %s\n", flag_strings[i]);
417 }
418
419 // again
420 iff->default_parse_flags2 = 0;
421 if (optional_string("$Default Ship Flags2:"))
422 {
423 i = 0;
424 j = 0;
425 char flag_strings[MAX_PARSE_OBJECT_FLAGS_2][NAME_LENGTH];
426 int num_strings = stuff_string_list(flag_strings, MAX_PARSE_OBJECT_FLAGS_2);
427 for (i = 0; i < num_strings; i++)
428 {
429 for (j = 0; j < MAX_PARSE_OBJECT_FLAGS_2; j++)
430 {
431 if (!stricmp(flag_strings[i], Parse_object_flags_2[j]))
432 {
433 iff->default_parse_flags2 |= (1 << j);
434 break;
435 }
436 }
437 }
438
439 if (j == MAX_PARSE_OBJECT_FLAGS_2)
440 Warning(LOCATION, "Bogus string in iff default ship flags2: %s\n", flag_strings[i]);
441 }
442
443 // this is cleared between each level but let's just set it here for thoroughness
444 iff->ai_rearm_timestamp = timestamp(-1);
445 }
446
447 required_string("#End");
448
449
450 // now resolve the relationships ------------------------------------------
451
452 // first get the traitor
453 Iff_traitor = iff_lookup(traitor_name);
454 if (Iff_traitor < 0)
455 {
456 Iff_traitor = 0;
457 Warning(LOCATION, "Traitor IFF %s not found in iff_defs.tbl! Defaulting to %s.\n", traitor_name, Iff_info[Iff_traitor].iff_name);
458 }
459
460 // next get the attackees and colors
461 for (int cur_iff = 0; cur_iff < Num_iffs; cur_iff++)
462 {
463 iff_info *iff = &Iff_info[cur_iff];
464
465 // clear the iffs to be attacked
466 iff->attackee_bitmask = 0;
467 iff->attackee_bitmask_all_teams_at_war = 0;
468
469 // clear the observed colors
470 for (j = 0; j < MAX_IFFS; j++)
471 iff->observed_color_index[j] = -1;
472
473 // resolve the list names
474 for (int list_index = 0; list_index < MAX_IFFS; list_index++)
475 {
476 // are we within the number of attackees listed?
477 if (list_index < num_attack_names[cur_iff])
478 {
479 // find out who
480 int target_iff = iff_lookup(attack_names[cur_iff][list_index]);
481
482 // valid?
483 if (target_iff >= 0)
484 iff->attackee_bitmask |= iff_get_mask(target_iff);
485 else
486 Warning(LOCATION, "Attack target IFF %s not found for IFF %s in iff_defs.tbl!\n", attack_names[cur_iff][list_index], iff->iff_name);
487 }
488
489 // are we within the number of colors listed?
490 if (list_index < num_observed_colors[cur_iff])
491 {
492 // find out who
493 int target_iff = iff_lookup(observed_color_table[cur_iff][list_index].iff_name);
494
495 // valid?
496 if (target_iff >= 0)
497 iff->observed_color_index[target_iff] = observed_color_table[cur_iff][list_index].color_index;
498 else
499 Warning(LOCATION, "Observed color IFF %s not found for IFF %s in iff_defs.tbl!\n", observed_color_table[cur_iff][list_index].iff_name, iff->iff_name);
500 }
501 }
502
503 // resolve the all teams at war relationships
504 if (iff->flags & IFFF_EXEMPT_FROM_ALL_TEAMS_AT_WAR)
505 {
506 // exempt, so use standard attacks
507 iff->attackee_bitmask_all_teams_at_war = iff->attackee_bitmask;
508 }
509 else
510 {
511 // nonexempt, so build bitmask of all other nonexempt teams
512 for (int other_iff = 0; other_iff < Num_iffs; other_iff++)
513 {
514 // skip myself (unless I attack myself normally)
515 if ((other_iff == cur_iff) && !iff_x_attacks_y(cur_iff, cur_iff))
516 continue;
517
518 // skip anyone exempt
519 if (Iff_info[other_iff].flags & IFFF_EXEMPT_FROM_ALL_TEAMS_AT_WAR)
520 continue;
521
522 // add everyone else
523 iff->attackee_bitmask_all_teams_at_war |= iff_get_mask(other_iff);
524 }
525 }
526 }
527
528 // add tbl/tbm to multiplayer validation list
529 extern void fs2netd_add_table_validation(const char *tblname);
530 fs2netd_add_table_validation("iff_defs.tbl");
531 }
532
533 /**
534 * Find the iff name
535 *
536 * @param iff_name Pointer to name as a string
537 * @return Index into ::Iff_info array
538 */
iff_lookup(char * iff_name)539 int iff_lookup(char *iff_name)
540 {
541 // bogus
542 Assert(iff_name);
543
544 if(iff_name == NULL)
545 return -1;
546
547 for (int i = 0; i < Num_iffs; i++)
548 if (!stricmp(iff_name, Iff_info[i].iff_name))
549 return i;
550
551 return -1;
552 }
553
554 /**
555 * Get the mask, taking All Teams At War into account
556 *
557 * @param attacker_team Team of attacker
558 * @return Bitmask
559 */
iff_get_attackee_mask(int attacker_team)560 int iff_get_attackee_mask(int attacker_team)
561 {
562 Assert(attacker_team >= 0 && attacker_team < Num_iffs);
563
564 // All teams attack all other teams.
565 if (Mission_all_attack)
566 {
567 return Iff_info[attacker_team].attackee_bitmask_all_teams_at_war;
568 }
569 // normal
570 else
571 {
572 return Iff_info[attacker_team].attackee_bitmask;
573 }
574 }
575
576 /**
577 * Rather slower, since it has to construct a mask
578 *
579 * @param attackee_team Team of attacker
580 * @return Bitmask
581 */
iff_get_attacker_mask(int attackee_team)582 int iff_get_attacker_mask(int attackee_team)
583 {
584 Assert(attackee_team >= 0 && attackee_team < Num_iffs);
585
586 int i, attacker_bitmask = 0;
587 for (i = 0; i < Num_iffs; i++)
588 {
589 if (iff_x_attacks_y(i, attackee_team))
590 attacker_bitmask |= iff_get_mask(i);
591 }
592
593 return attacker_bitmask;
594 }
595
596 /**
597 * Similar to above
598 *
599 * @param team_x Team of attacker
600 * @param team_y Team of attackee
601 *
602 * @return >0 if true, 0 if false
603 */
iff_x_attacks_y(int team_x,int team_y)604 int iff_x_attacks_y(int team_x, int team_y)
605 {
606 return iff_matches_mask(team_y, iff_get_attackee_mask(team_x));
607 }
608
609 /**
610 * Generate a mask for a team
611 *
612 * @param team Team to generate mask for
613 */
iff_get_mask(int team)614 int iff_get_mask(int team)
615 {
616 return (1 << team);
617 }
618
619 /**
620 * See if the mask contains the team
621 *
622 * @param team Team to test
623 * @param mask Mask
624 *
625 * @return 1 if matches, 0 if does not match
626 */
iff_matches_mask(int team,int mask)627 int iff_matches_mask(int team, int mask)
628 {
629 return (iff_get_mask(team) & mask) ? 1 : 0;
630 }
631
632 /**
633 * Get the color from the color index
634 */
iff_get_color(int color_index,int is_bright)635 color *iff_get_color(int color_index, int is_bright)
636 {
637 return &Iff_colors[color_index][is_bright];
638 }
639
640 /**
641 * Get the color index, taking objective vs. subjective into account
642 */
iff_get_color_by_team(int team,int seen_from_team,int is_bright)643 color *iff_get_color_by_team(int team, int seen_from_team, int is_bright)
644 {
645 Assert(team >= 0 && team < Num_iffs);
646 Assert(seen_from_team < Num_iffs);
647 Assert(is_bright == 0 || is_bright == 1);
648
649
650 // is this guy being seen by anyone?
651 if (seen_from_team < 0)
652 return &Iff_colors[Iff_info[team].color_index][is_bright];
653
654 // Goober5000 - base the following on "sees X as" from iff code
655 // c.f. AL's comment:
656
657 // AL 12-26-97: it seems IFF color needs to be set relative to the player team. If
658 // the team in question is the same as the player, then it should be
659 // drawn friendly. If the team is different than the player's, then draw the
660 // appropriate IFF.
661
662
663 // assume an observed color is defined; if not, use normal color
664 int color_index = Iff_info[seen_from_team].observed_color_index[team];
665 if (color_index < 0)
666 color_index = Iff_info[team].color_index;
667
668
669 return &Iff_colors[color_index][is_bright];
670 }
671
672 /**
673 * Get the color index, taking objective vs. subjective into account
674 *
675 * this one for the function calls that include some - any - of object
676 */
iff_get_color_by_team_and_object(int team,int seen_from_team,int is_bright,object * objp)677 color *iff_get_color_by_team_and_object(int team, int seen_from_team, int is_bright, object *objp)
678 {
679 Assert(team >= 0 && team < Num_iffs);
680 Assert(seen_from_team < Num_iffs);
681 Assert(is_bright == 0 || is_bright == 1);
682
683 int alt_color_index = -1;
684
685 // is this guy being seen by anyone?
686 if (seen_from_team < 0)
687 return &Iff_colors[Iff_info[team].color_index][is_bright];
688
689 int color_index = Iff_info[seen_from_team].observed_color_index[team];
690
691 // switch incase some sort of parent iff color inheritance for example for bombs is wanted...
692 switch(objp->type)
693 {
694 case OBJ_SHIP:
695 if (Ships[objp->instance].ship_iff_color[seen_from_team][team] >= 0)
696 {
697 alt_color_index = Ships[objp->instance].ship_iff_color[seen_from_team][team];
698 }
699 else
700 {
701 alt_color_index = Ship_info[Ships[objp->instance].ship_info_index].ship_iff_info[seen_from_team][team];
702 }
703 break;
704 default:
705 break;
706 }
707
708 // temporary solution....
709 if (alt_color_index >= 0)
710 color_index = alt_color_index;
711 if (color_index < 0)
712 color_index = Iff_info[team].color_index;
713
714
715 return &Iff_colors[color_index][is_bright];
716 }
717