1 /**
2 * @file
3 * @brief Functions related to the monster arena (stage and watch fights).
4 **/
5
6 #include "AppHdr.h"
7
8 #include "arena.h"
9
10 #include <stdexcept>
11
12 #include "act-iter.h"
13 #include "colour.h"
14 #include "command.h"
15 #include "dungeon.h"
16 #include "end.h"
17 #include "initfile.h"
18 #include "item-name.h"
19 #include "item-status-flag-type.h"
20 #include "items.h"
21 #include "libutil.h"
22 #include "los.h"
23 #include "macro.h"
24 #include "maps.h"
25 #include "menu.h"
26 #include "message.h"
27 #include "mgen-data.h"
28 #include "mon-death.h"
29 #include "mon-pick.h"
30 #include "mon-tentacle.h"
31 #include "newgame-def.h"
32 #include "ng-init.h"
33 #include "spl-miscast.h"
34 #include "state.h"
35 #include "stringutil.h"
36 #include "teleport.h"
37 #include "terrain.h"
38 #ifdef USE_TILE
39 #include "tileview.h"
40 #endif
41 #include "unicode.h"
42 #include "unique-creature-list-type.h"
43 #include "version.h"
44 #include "view.h"
45 #include "ui.h"
46
47 using namespace ui;
48
49 #define ARENA_VERBOSE
50
51 namespace msg
52 {
53 // wrap a message tee around a file ptr, which can be null.
54 // for a more general purpose application you'd want this to handle opening
55 // and closing the file too, but that would require some restructuring of the
56 // arena.
57 class arena_tee : tee
58 {
59 public:
arena_tee(FILE ** _file)60 arena_tee(FILE **_file) : tee(), file(_file) { }
61
~arena_tee()62 ~arena_tee()
63 {
64 if (*file)
65 fflush(*file);
66 }
67
append(const string & s,msg_channel_type ch=MSGCH_PLAIN)68 void append(const string &s, msg_channel_type ch = MSGCH_PLAIN)
69 {
70 if (Options.arena_dump_msgs && *file)
71 {
72 if (!s.size())
73 return;
74 string prefix;
75 switch (ch)
76 {
77 case MSGCH_DIAGNOSTICS:
78 prefix = "DIAG: ";
79 if (Options.arena_dump_msgs_all)
80 break;
81 return;
82
83 // Ignore messages generated while the user examines
84 // the arnea.
85 case MSGCH_PROMPT:
86 case MSGCH_MONSTER_TARGET:
87 case MSGCH_FLOOR_ITEMS:
88 case MSGCH_EXAMINE:
89 case MSGCH_EXAMINE_FILTER:
90 return;
91
92 // If a monster-damage message ends with '!' it's a
93 // death message, otherwise it's an examination message
94 // and should be skipped.
95 case MSGCH_MONSTER_DAMAGE:
96 if (s[s.size() - 1] != '!')
97 return;
98 break;
99
100 case MSGCH_ERROR: prefix = "ERROR: "; break;
101 case MSGCH_WARN: prefix = "WARN: "; break;
102 case MSGCH_SOUND: prefix = "SOUND: "; break;
103
104 case MSGCH_TALK_VISUAL:
105 case MSGCH_TALK: prefix = "TALK: "; break;
106 default: break;
107 }
108 formatted_string fs = formatted_string::parse_string(s);
109 fprintf(*file, "%s%s", prefix.c_str(), fs.tostring().c_str());
110 fflush(*file);
111 }
112 }
113
114 private:
115 FILE **file;
116 };
117 }
118
119 extern void world_reacts();
120
_results_popup(string msg,bool error=false)121 static void _results_popup(string msg, bool error=false)
122 {
123 // TODO: shared code here with end.cc
124 linebreak_string(msg, 79);
125
126 #ifdef USE_TILE_WEB
127 tiles_crt_popup show_as_popup;
128 tiles.set_ui_state(UI_CRT);
129 #endif
130
131 if (error)
132 {
133 msg = string("Arena error:\n\n<lightred>")
134 + replace_all(msg, "<", "<<");
135 msg += "</lightred>";
136 }
137 else
138 msg = string("Arena results:\n\n") + msg;
139
140 msg += "\n\n<cyan>Hit any key to continue, "
141 "ctrl-p for the full log.</cyan>";
142
143 auto prompt_ui = make_shared<Text>(
144 formatted_string::parse_string(msg));
145 bool done = false;
146 prompt_ui->on_hotkey_event([&](const KeyEvent& ev) {
147 if (ev.key() == CONTROL('P'))
148 replay_messages();
149 else
150 done = true;
151 return done;
152 });
153
154 mouse_control mc(MOUSE_MODE_MORE);
155 auto popup = make_shared<ui::Popup>(prompt_ui);
156 ui::run_layout(move(popup), done);
157 }
158
159 namespace arena
160 {
161 static bool skipped_arena_ui = true; // whether this is an interactive session
162 static void write_error(const string &error);
163
164 struct arena_error : public runtime_error
165 {
arena_errorarena::arena_error166 explicit arena_error(const string &msg, bool _fatal=true)
167 : runtime_error(msg), fatal(_fatal) {}
arena_errorarena::arena_error168 explicit arena_error(const char *msg, bool _fatal=true)
169 : runtime_error(msg), fatal(_fatal) {}
170 bool fatal;
171 };
172 #define arena_error_f(...) arena_error(make_stringf(__VA_ARGS__))
173 #define arena_error_nonfatal_f(...) arena_error(make_stringf(__VA_ARGS__), false)
174
175 // A faction is just a big list of monsters. Monsters will be dropped
176 // around the appropriate marker.
177 struct faction
178 {
179 string desc;
180 mons_list members;
181 bool friendly;
182 int active_members;
183 bool won;
184
185 vector<int> respawn_list;
186 vector<coord_def> respawn_pos;
187
factionarena::faction188 faction(bool fr) : members(), friendly(fr), active_members(0),
189 won(false) { }
190
191 void place_at(const coord_def &pos);
192
resetarena::faction193 void reset()
194 {
195 active_members = 0;
196 won = false;
197
198 respawn_list.clear();
199 respawn_pos.clear();
200 }
201
cleararena::faction202 void clear()
203 {
204 reset();
205 members.clear();
206 }
207 };
208
209 static string teams;
210
211 static int total_trials = 0;
212
213 static bool contest_cancelled = false;
214
215 static bool is_respawning = false;
216
217 static int trials_done = 0;
218 static int team_a_wins = 0;
219 static int ties = 0;
220
221 static int turns = 0;
222
223 static bool allow_summons = true;
224 static bool allow_animate = true;
225 static bool allow_chain_summons = true;
226 static bool allow_zero_xp = false;
227 static bool allow_immobile = true;
228 static bool allow_bands = true;
229 static bool name_monsters = false;
230 static bool random_uniques = false;
231 static bool real_summons = false;
232 static bool move_summons = false;
233 static bool respawn = false;
234 static bool move_respawns = false;
235
236 static bool miscasts = false;
237
238 static int summon_throttle = INT_MAX;
239
240 static vector<monster_type> uniques_list;
241 static vector<int> a_spawners;
242 static vector<int> b_spawners;
243 static int8_t to_respawn[MAX_MONSTERS];
244
245 static int item_drop_times[MAX_ITEMS];
246
247 static bool banned_glyphs[128];
248
249 static string arena_type = "";
250 static faction faction_a(true);
251 static faction faction_b(false);
252 static coord_def place_a, place_b;
253
254 static bool cycle_random = false;
255 static uint32_t cycle_random_pos = 0;
256
257 static FILE *file = nullptr;
258 static level_id place(BRANCH_DEPTHS, 1);
259 static string arena_log;
260
adjust_spells(monster * mons,bool no_summons,bool no_animate)261 static void adjust_spells(monster* mons, bool no_summons, bool no_animate)
262 {
263 monster_spells &spells(mons->spells);
264 erase_if(spells, [&](const mon_spell_slot &t) {
265 return (no_summons && spell_typematch(t.spell, spschool::summoning))
266 || (no_animate && t.spell == SPELL_ANIMATE_DEAD);
267 });
268 }
269
adjust_monsters()270 static void adjust_monsters()
271 {
272 for (monster_iterator mon; mon; ++mon)
273 {
274 const bool friendly = mon->friendly();
275 // Set target to the opposite faction's home base.
276 mon->target = friendly ? place_b : place_a;
277 }
278 }
279
list_eq(const monster * mon)280 static void list_eq(const monster *mon)
281 {
282 if (!Options.arena_list_eq || file == nullptr)
283 return;
284
285 vector<int> items;
286 for (short it : mon->inv)
287 if (it != NON_ITEM)
288 items.push_back(it);
289
290 if (items.empty())
291 return;
292
293 fprintf(file, "%s:\n", mon->name(DESC_PLAIN, true).c_str());
294
295 for (int iidx : items)
296 {
297 item_def &item = env.item[iidx];
298 fprintf(file, " %s\n",
299 item.name(DESC_PLAIN, false, true).c_str());
300 }
301 }
302
place_at(const coord_def & pos)303 void faction::place_at(const coord_def &pos)
304 {
305 ASSERT_IN_BOUNDS(pos);
306 for (int i = 0, size = members.size(); i < size; ++i)
307 {
308 mons_spec spec = members.get_monster(i);
309
310 if (friendly)
311 spec.attitude = ATT_FRIENDLY;
312
313 for (int q = 0; q < spec.quantity; ++q)
314 {
315 const coord_def loc = pos;
316 if (!in_bounds(loc))
317 break;
318
319 const monster* mon;
320 if (mons_class_requires_band(spec.type) && !spec.band)
321 {
322 unwind_bool no(allow_bands, false);
323 unwind_bool yes(spec.band, true);
324 mon = dgn_place_monster(spec, loc, false, true, false);
325 }
326 else
327 mon = dgn_place_monster(spec, loc, false, true, false);
328 if (!mon)
329 {
330 game_ended_with_error(
331 make_stringf(
332 "Failed to create monster at (%d,%d) env.grid: %s",
333 loc.x, loc.y, dungeon_feature_name(env.grid(loc))));
334 }
335 list_eq(mon);
336 to_respawn[mon->mindex()] = i;
337 }
338 }
339 }
340
center_print(unsigned sz,string text,int number=-1)341 static void center_print(unsigned sz, string text, int number = -1)
342 {
343 if (number >= 0)
344 text = make_stringf("(%d) %s", number, text.c_str());
345
346 unsigned len = strwidth(text);
347 if (len > sz)
348 text = chop_string(text, len = sz);
349
350 cprintf("%s%s", string((sz - len) / 2, ' ').c_str(), text.c_str());
351 }
352
setup_level()353 static void setup_level()
354 {
355 turns = 0;
356
357 a_spawners.clear();
358 b_spawners.clear();
359 memset(item_drop_times, 0, sizeof(item_drop_times));
360
361 if (place.is_valid())
362 {
363 you.where_are_you = place.branch;
364 you.depth = place.depth;
365 }
366
367 dgn_reset_level();
368
369 for (int x = 0; x < GXM; ++x)
370 for (int y = 0; y < GYM; ++y)
371 env.grid[x][y] = DNGN_ROCK_WALL;
372
373 unwind_bool gen(crawl_state.generating_level, true);
374
375 typedef unwind_var< set<string> > unwind_stringset;
376
377 const unwind_stringset mtags(you.uniq_map_tags);
378 const unwind_stringset mnames(you.uniq_map_names);
379
380 string map_name = "arena_" + arena_type;
381 const map_def *map = random_map_for_tag(map_name);
382
383 if (!map)
384 {
385 throw arena_error_f("No arena maps named \"%s\"",
386 arena_type.c_str());
387 }
388
389 #ifdef USE_TILE
390 // Arena is never saved, so we can skip this.
391 tile_init_default_flavour();
392 tile_clear_flavour();
393 #endif
394
395 ASSERT(map);
396 bool success = dgn_place_map(map, false, true);
397 if (!success)
398 {
399 throw arena_error_f("Failed to create arena named \"%s\"",
400 arena_type.c_str());
401 }
402 link_items();
403
404 if (!env.rock_colour)
405 env.rock_colour = CYAN;
406 if (!env.floor_colour)
407 env.floor_colour = LIGHTGREY;
408
409 #ifdef USE_TILE
410 tile_new_level(true);
411 #endif
412 los_changed();
413 env.markers.activate_all();
414 }
415
find_monster_spec()416 static string find_monster_spec()
417 {
418 if (!teams.empty())
419 return teams;
420 else
421 return "random v random";
422 }
423
424 /// @throws arena_error if a monster specification is invalid.
parse_faction(faction & fact,string spec)425 static void parse_faction(faction &fact, string spec)
426 {
427 fact.clear();
428 fact.desc = spec;
429
430 for (const string &monster : split_string(",", spec))
431 {
432 const string err = fact.members.add_mons(monster, false);
433 if (!err.empty())
434 throw arena_error(err, false);
435 }
436 }
437
438 /// @throws arena_error if the monster specification is invalid.
parse_monster_spec()439 static void parse_monster_spec()
440 {
441 string spec = find_monster_spec();
442
443 allow_chain_summons = !strip_tag(spec, "no_chain_summons");
444
445 allow_summons = !strip_tag(spec, "no_summons");
446 allow_animate = !strip_tag(spec, "no_animate");
447 allow_immobile = !strip_tag(spec, "no_immobile");
448 allow_bands = !strip_tag(spec, "no_bands");
449 allow_zero_xp = strip_tag(spec, "allow_zero_xp");
450 real_summons = strip_tag(spec, "real_summons");
451 move_summons = strip_tag(spec, "move_summons");
452 miscasts = strip_tag(spec, "miscasts");
453 respawn = strip_tag(spec, "respawn");
454 move_respawns = strip_tag(spec, "move_respawns");
455 summon_throttle = strip_number_tag(spec, "summon_throttle:");
456
457 if (real_summons && respawn)
458 {
459 throw arena_error("Can't set real_summons and respawn at same time.",
460 false);
461 }
462
463 if (summon_throttle <= 0)
464 summon_throttle = INT_MAX;
465
466 cycle_random = strip_tag(spec, "cycle_random");
467 name_monsters = strip_tag(spec, "names");
468 random_uniques = strip_tag(spec, "random_uniques");
469
470 const int ntrials = strip_number_tag(spec, "t:");
471 if (ntrials != TAG_UNFOUND && ntrials >= 1 && ntrials <= 99
472 && !total_trials)
473 {
474 total_trials = ntrials;
475 }
476
477 arena_type = strip_tag_prefix(spec, "arena:");
478
479 if (arena_type.empty())
480 arena_type = "default";
481
482 const int arena_delay = strip_number_tag(spec, "delay:");
483 if (arena_delay >= 0 && arena_delay < 2000)
484 Options.view_delay = arena_delay;
485
486 string arena_place = strip_tag_prefix(spec, "arena_place:");
487 if (!arena_place.empty())
488 {
489 try
490 {
491 place = level_id::parse_level_id(arena_place);
492 }
493 catch (const bad_level_id &err)
494 {
495 throw arena_error_nonfatal_f("Bad place '%s': %s",
496 arena_place.c_str(),
497 err.what());
498 }
499 }
500
501 for (unsigned char gly : strip_tag_prefix(spec, "ban_glyphs:"))
502 if (gly < ARRAYSZ(banned_glyphs))
503 banned_glyphs[gly] = true;
504
505 vector<string> factions = split_string(" v ", spec);
506
507 if (factions.size() == 1)
508 factions = split_string(" vs ", spec);
509
510 if (factions.size() != 2)
511 {
512 throw arena_error_nonfatal_f(
513 "Expected arena monster spec \"xxx v yyy\", "
514 "but got \"%s\"", spec.c_str());
515 }
516
517 try
518 {
519 parse_faction(faction_a, factions[0]);
520 parse_faction(faction_b, factions[1]);
521 }
522 catch (const arena_error &err)
523 {
524 throw arena_error_nonfatal_f("Bad monster spec \"%s\": %s",
525 spec.c_str(),
526 err.what());
527 }
528
529 if (faction_a.desc == faction_b.desc)
530 {
531 faction_a.desc += " (A)";
532 faction_b.desc += " (B)";
533 }
534 }
535
setup_monsters()536 static void setup_monsters()
537 {
538 faction_a.reset();
539 faction_b.reset();
540
541 for (int i = 0; i < MAX_MONSTERS; i++)
542 to_respawn[i] = -1;
543
544 unwind_var<unique_creature_list> uniq(you.unique_creatures);
545
546 place_a = dgn_find_feature_marker(DNGN_STONE_STAIRS_UP_I);
547 place_b = dgn_find_feature_marker(DNGN_STONE_STAIRS_DOWN_I);
548
549 // Place the different factions in different orders on
550 // alternating rounds so that one side doesn't get the
551 // first-move advantage for all rounds.
552 if (trials_done & 1)
553 {
554 faction_a.place_at(place_a);
555 faction_b.place_at(place_b);
556 }
557 else
558 {
559 faction_b.place_at(place_b);
560 faction_a.place_at(place_a);
561 }
562
563 adjust_monsters();
564 }
565
show_fight_banner(bool after_fight=false)566 static void show_fight_banner(bool after_fight = false)
567 {
568 int line = 1;
569
570 cgotoxy(1, line++, GOTO_STAT);
571 textcolour(WHITE);
572 center_print(crawl_view.hudsz.x, string("Crawl ") + Version::Long);
573 line++;
574
575 cgotoxy(1, line++, GOTO_STAT);
576 textcolour(YELLOW);
577 center_print(crawl_view.hudsz.x, faction_a.desc,
578 total_trials ? team_a_wins : -1);
579 cgotoxy(1, line++, GOTO_STAT);
580 textcolour(LIGHTGREY);
581 center_print(crawl_view.hudsz.x, "vs");
582 cgotoxy(1, line++, GOTO_STAT);
583 textcolour(YELLOW);
584 center_print(crawl_view.hudsz.x, faction_b.desc,
585 total_trials ? trials_done - team_a_wins - ties : -1);
586
587 if (total_trials > 1 && trials_done < total_trials)
588 {
589 cgotoxy(1, line++, GOTO_STAT);
590 textcolour(BROWN);
591 center_print(crawl_view.hudsz.x,
592 make_stringf("Round %d of %d",
593 after_fight ? trials_done
594 : trials_done + 1,
595 total_trials));
596 }
597 else
598 {
599 cgotoxy(1, line++, GOTO_STAT);
600 textcolour(BROWN);
601 clear_to_end_of_line();
602 }
603 }
604
setup_others()605 static void setup_others()
606 {
607 you.species = SP_HUMAN;
608 you.char_class = JOB_FIGHTER;
609 you.experience_level = 27;
610
611 you.position.y = -1;
612 coord_def yplace(dgn_find_feature_marker(DNGN_ESCAPE_HATCH_UP));
613 crawl_view.set_player_at(yplace);
614
615 you.mutation[MUT_ACUTE_VISION] = 3;
616
617 you.your_name = "Arena";
618
619 you.hp = you.hp_max = 99;
620
621 for (int i = 0; i < NUM_STATS; ++i)
622 you.base_stats[i] = 20;
623
624 // XXX: now that you.species is valid, do a layout.
625 // This is necessary to ensure that the stat window is positioned.
626 #ifdef USE_TILE
627 tiles.resize();
628 #endif
629
630 show_fight_banner();
631 }
632
expand_mlist(int exp)633 static void expand_mlist(int exp)
634 {
635 crawl_view.mlistp.y -= exp;
636 crawl_view.mlistsz.y += exp;
637 }
638
639 /// @throws arena_error if the specification was invalid.
setup_fight()640 static void setup_fight()
641 {
642 //msg::suppress mx;
643 parse_monster_spec();
644 setup_level();
645
646 // Monster setup may block waiting for matchups.
647 setup_monsters();
648
649 setup_others();
650 }
651
count_foes()652 static void count_foes()
653 {
654 int orig_a = faction_a.active_members;
655 int orig_b = faction_b.active_members;
656
657 if (orig_a < 0)
658 mprf(MSGCH_ERROR, "Book-keeping says faction_a has negative active members.");
659
660 if (orig_b < 0)
661 mprf(MSGCH_ERROR, "Book-keeping says faction_b has negative active members.");
662
663 faction_a.active_members = 0;
664 faction_b.active_members = 0;
665
666 for (monster_iterator mons; mons; ++mons)
667 {
668 if (mons_is_tentacle_or_tentacle_segment(mons->type))
669 continue;
670 if (mons->attitude == ATT_FRIENDLY)
671 faction_a.active_members++;
672 else if (mons->attitude == ATT_HOSTILE)
673 faction_b.active_members++;
674 }
675
676 if (orig_a != faction_a.active_members
677 || orig_b != faction_b.active_members)
678 {
679 mprf(MSGCH_ERROR, "Book-keeping error in faction member count: "
680 "%d:%d instead of %d:%d",
681 orig_a, orig_b,
682 faction_a.active_members, faction_b.active_members);
683
684 if (faction_a.active_members > 0
685 && faction_b.active_members <= 0)
686 {
687 faction_a.won = true;
688 faction_b.won = false;
689 }
690 else if (faction_b.active_members > 0
691 && faction_a.active_members <= 0)
692 {
693 faction_b.won = true;
694 faction_a.won = false;
695 }
696 }
697 }
698
699 // Returns true as long as at least one member of each faction is alive.
fight_is_on()700 static bool fight_is_on()
701 {
702 if (faction_a.active_members > 0 && faction_b.active_members > 0)
703 {
704 if (faction_a.won || faction_b.won)
705 {
706 mprf(MSGCH_ERROR, "Both factions alive but one declared the winner.");
707 faction_a.won = false;
708 faction_b.won = false;
709 }
710 return true;
711 }
712
713 // Sync up our book-keeping with the actual state, and report
714 // any inconsistencies.
715 count_foes();
716
717 return faction_a.active_members > 0 && faction_b.active_members > 0;
718 }
719
720 // Try to prevent random luck from letting one spawner fill up the
721 // arena with so many monsters that the other spawner can never get
722 // back on even footing.
balance_spawners()723 static void balance_spawners()
724 {
725 if (a_spawners.empty() || b_spawners.empty())
726 return;
727
728 if (faction_a.active_members == 0 || faction_b.active_members == 0)
729 {
730 mprf(MSGCH_ERROR, "ERROR: Both sides have spawners, but the active "
731 "member count of one side has been reduced to zero!");
732 return;
733 }
734
735 for (int idx : a_spawners)
736 {
737 env.mons[idx].speed_increment *= faction_b.active_members;
738 env.mons[idx].speed_increment /= faction_a.active_members;
739 }
740
741 for (int idx : b_spawners)
742 {
743 env.mons[idx].speed_increment *= faction_a.active_members;
744 env.mons[idx].speed_increment /= faction_b.active_members;
745 }
746 }
747
do_miscasts()748 static void do_miscasts()
749 {
750 if (!miscasts)
751 return;
752
753 for (monster_iterator mon; mon; ++mon)
754 {
755 if (mon->type == MONS_TEST_SPAWNER)
756 continue;
757
758 if (!mon->alive())
759 continue;
760
761 miscast_effect(**mon, *mon, {miscast_source::wizard},
762 spschool::random, random_range(1, 9),
763 random_range(1, 100), "arena miscast");
764 }
765 }
766
handle_keypress(int ch)767 static void handle_keypress(int ch)
768 {
769 if (key_is_escape(ch) || toalower(ch) == 'q')
770 {
771 contest_cancelled = true;
772 return;
773 }
774
775 const command_type cmd = key_to_command(ch, KMC_DEFAULT);
776
777 // We only allow a short list of commands to be used in the arena.
778 switch (cmd)
779 {
780 case CMD_LOOK_AROUND:
781 case CMD_SUSPEND_GAME:
782 case CMD_REPLAY_MESSAGES:
783 break;
784
785 default:
786 return;
787 }
788
789 if (file != nullptr)
790 fflush(file);
791
792 cursor_control coff(true);
793
794 unwind_var<game_type> type(crawl_state.type, GAME_TYPE_NORMAL);
795 unwind_bool ar_susp(crawl_state.arena_suspended, true);
796 coord_def yplace(dgn_find_feature_marker(DNGN_ESCAPE_HATCH_UP));
797 unwind_var<coord_def> pos(you.position);
798 you.position = yplace;
799 process_command(cmd);
800 }
801
do_respawn(faction & fac)802 static void do_respawn(faction &fac)
803 {
804 is_respawning = true;
805 for (unsigned int _i = fac.respawn_list.size(); _i > 0; _i--)
806 {
807 unsigned int i = _i - 1;
808
809 coord_def pos = fac.respawn_pos[i];
810 int spec_idx = fac.respawn_list[i];
811 mons_spec spec = fac.members.get_monster(spec_idx);
812
813 if (fac.friendly)
814 spec.attitude = ATT_FRIENDLY;
815
816 monster *mon = dgn_place_monster(spec, pos, false, true);
817
818 if (!mon && fac.active_members == 0 && monster_at(pos))
819 {
820 // We have no members left, so to prevent the round
821 // from ending attempt to displace whatever is in
822 // our position.
823 monster& other = *monster_at(pos);
824
825 if (to_respawn[other.mindex()] == -1)
826 {
827 // The other monster isn't a respawner itself, so
828 // just get rid of it.
829 mprf(MSGCH_DIAGNOSTICS,
830 "Dismissing non-respawner %s to make room for "
831 "respawner whose side has 0 active members.",
832 other.name(DESC_PLAIN, true).c_str());
833 monster_die(other, KILL_DISMISSED, NON_MONSTER);
834 }
835 else
836 {
837 // Other monster is a respawner, try to move it.
838 mprf(MSGCH_DIAGNOSTICS,
839 "Teleporting respawner %s to make room for "
840 "other respawner whose side has 0 active members.",
841 other.name(DESC_PLAIN, true).c_str());
842 monster_teleport(&other, true);
843 }
844
845 mon = dgn_place_monster(spec, pos, false, true);
846 }
847
848 if (mon)
849 {
850 // We succeeded, so remove from list.
851 fac.respawn_list.erase(fac.respawn_list.begin() + i);
852 fac.respawn_pos.erase(fac.respawn_pos.begin() + i);
853
854 to_respawn[mon->mindex()] = spec_idx;
855
856 if (move_respawns)
857 monster_teleport(mon, true, true);
858 }
859 else
860 {
861 // Couldn't respawn, so leave it on the list; hopefully
862 // space will open up later.
863 }
864 }
865 is_respawning = false;
866 }
867
do_fight()868 static void do_fight()
869 {
870 viewwindow();
871 update_screen();
872 clear_messages(true);
873
874 {
875 cursor_control coff(false);
876 while (fight_is_on() && !contest_cancelled)
877 {
878 #ifdef ARENA_VERBOSE
879 mprf("---- Turn #%d ----", turns);
880 #endif
881
882 // Check the consistency of our book-keeping every 100 turns.
883 if ((turns++ % 100) == 0)
884 count_foes();
885
886 you.time_taken = 10;
887 //report_foes();
888 world_reacts();
889 do_miscasts();
890 do_respawn(faction_a);
891 do_respawn(faction_b);
892 balance_spawners();
893 ui::delay(Options.view_delay);
894 clear_messages();
895 ASSERT(you.pet_target == MHITNOT);
896 }
897 viewwindow();
898 update_screen();
899 }
900
901 if (contest_cancelled)
902 {
903 mpr("Cancelled contest at user request");
904 ui::delay(Options.view_delay);
905 clear_messages();
906 return;
907 }
908
909 clear_messages();
910
911 trials_done++;
912
913 // We bother with all this to properly deal with ties, and with
914 // ball lightning or ballistomycete spores winning the fight via suicide.
915 // The sanity checking is probably just paranoia.
916 bool was_tied = false;
917 if (!faction_a.won && !faction_b.won)
918 {
919 if (faction_a.active_members > 0)
920 {
921 mprf(MSGCH_ERROR, "Tie declared, but faction_a won.");
922 team_a_wins++;
923 faction_a.won = true;
924 }
925 else if (faction_b.active_members > 0)
926 {
927 mprf(MSGCH_ERROR, "Tie declared, but faction_b won.");
928 faction_b.won = true;
929 }
930 else
931 {
932 ties++;
933 was_tied = true;
934 }
935 }
936 else if (faction_a.won && faction_b.won)
937 {
938 faction_a.won = false;
939 faction_b.won = false;
940
941 mprf(MSGCH_ERROR, "*BOTH* factions won?!");
942 if (faction_a.active_members > 0)
943 {
944 mprf(MSGCH_ERROR, "Faction_a real winner.");
945 team_a_wins++;
946 faction_a.won = true;
947 }
948 else if (faction_b.active_members > 0)
949 {
950 mprf(MSGCH_ERROR, "Faction_b real winner.");
951 faction_b.won = true;
952 }
953 else
954 {
955 mprf(MSGCH_ERROR, "Both sides dead.");
956 ties++;
957 was_tied = true;
958 }
959 }
960 else if (faction_a.won)
961 team_a_wins++;
962
963 show_fight_banner(true);
964
965 string msg;
966 if (was_tied)
967 msg = "Tie";
968 else
969 msg = "Winner: %s!";
970
971 if (Options.arena_dump_msgs || Options.arena_list_eq)
972 msg = "---------- " + msg + " ----------";
973
974 if (was_tied)
975 mpr(msg);
976 else
977 mprf(msg.c_str(),
978 faction_a.won ? faction_a.desc.c_str()
979 : faction_b.desc.c_str());
980 }
981
global_setup(const string & arena_teams)982 static void global_setup(const string& arena_teams)
983 {
984 // Clear some things that shouldn't persist across restart_after_game.
985 // parse_monster_spec and setup_fight will clear the rest.
986 total_trials = trials_done = team_a_wins = ties = 0;
987 contest_cancelled = false;
988 is_respawning = false;
989 uniques_list.clear();
990 memset(banned_glyphs, 0, sizeof(banned_glyphs));
991 arena_type = "";
992 place = level_id(BRANCH_DEPTHS, 1);
993 arena_log = "";
994
995 // [ds] Turning off view_lock crashes arena.
996 Options.view_lock_x = Options.view_lock_y = true;
997
998 teams = arena_teams;
999 // Set various options from the arena spec's tags
1000 parse_monster_spec(); // may throw an arena_error
1001
1002 crawl_view.init_geometry();
1003 expand_mlist(5);
1004
1005 for (monster_type i = MONS_0; i < NUM_MONSTERS; ++i)
1006 {
1007 if (i == MONS_PLAYER_GHOST)
1008 continue;
1009
1010 if (mons_is_unique(i) && !arena_veto_random_monster(i))
1011 uniques_list.push_back(i);
1012 }
1013 }
1014
global_shutdown()1015 static void global_shutdown()
1016 {
1017 if (file != nullptr)
1018 fclose(file);
1019
1020 file = nullptr;
1021 arena_log = "";
1022 }
1023
write_results()1024 static void write_results()
1025 {
1026 if (file != nullptr)
1027 {
1028 if (Options.arena_dump_msgs || Options.arena_list_eq)
1029 fprintf(file, "========================================\n");
1030 fprintf(file, "%d-%d", team_a_wins,
1031 trials_done - team_a_wins - ties);
1032 if (ties > 0)
1033 fprintf(file, "-%d", ties);
1034 fprintf(file, "\n");
1035 }
1036 }
1037
write_error(const string & error)1038 static void write_error(const string &error)
1039 {
1040 if (file != nullptr)
1041 {
1042 fprintf(file, "err: %s\n", error.c_str());
1043 fclose(file);
1044 }
1045 file = nullptr;
1046 }
1047
simulate()1048 static void simulate()
1049 {
1050 init_level_connectivity();
1051
1052 class UIArena : public Box
1053 {
1054 public:
1055 UIArena() : Box(Widget::VERT) {
1056 expand_h = expand_v = true;
1057 };
1058 virtual void _render() override {};
1059 virtual void _allocate_region() override {
1060 show_fight_banner();
1061 viewwindow();
1062 update_screen();
1063 display_message_window();
1064 };
1065 virtual bool on_event(const Event& ev) override {
1066 if (ev.type() != Event::Type::KeyDown)
1067 return false;
1068 handle_keypress(static_cast<const KeyEvent&>(ev).key());
1069 ASSERT(crawl_state.game_is_arena());
1070 ASSERT(!crawl_state.arena_suspended);
1071 return true;
1072 };
1073 };
1074
1075 auto ui = make_shared<UIArena>();
1076 ui::push_layout(ui);
1077
1078 do
1079 {
1080 try
1081 {
1082 setup_fight();
1083 }
1084 catch (const arena_error &error)
1085 {
1086 write_error(error.what());
1087 game_ended_with_error(error.what());
1088 continue;
1089 }
1090 do_fight();
1091
1092 if (trials_done < total_trials)
1093 ui::delay(Options.view_delay * 5);
1094 }
1095 while (!contest_cancelled && trials_done < total_trials);
1096
1097 ui::delay(Options.view_delay * 5);
1098
1099 if (total_trials > 0)
1100 {
1101 string outcome = make_stringf(
1102 "Final score: %s (%d); %s (%d) [%d ties]",
1103 faction_a.desc.c_str(), team_a_wins,
1104 faction_b.desc.c_str(), trials_done - team_a_wins - ties,
1105 ties);
1106 mpr(outcome);
1107 if (!skipped_arena_ui)
1108 _results_popup(outcome);
1109 }
1110
1111 ui::pop_layout();
1112
1113 write_results();
1114 }
1115 }
1116
1117 /////////////////////////////////////////////////////////////////////////////
1118
1119 // Various arena callbacks
1120
arena_pick_random_monster(const level_id & place)1121 monster_type arena_pick_random_monster(const level_id &place)
1122 {
1123 if (arena::random_uniques)
1124 {
1125 const vector<monster_type> &uniques = arena::uniques_list;
1126
1127 const monster_type type = uniques[random2(uniques.size())];
1128 you.unique_creatures.set(type, false);
1129
1130 return type;
1131 }
1132
1133 if (!arena::cycle_random)
1134 return RANDOM_MONSTER;
1135
1136 for (int tries = 0; tries <= NUM_MONSTERS; tries++)
1137 {
1138 monster_type mons = pick_monster_by_hash(place.branch,
1139 ++arena::cycle_random_pos);
1140
1141 if (arena_veto_random_monster(mons))
1142 continue;
1143
1144 return mons;
1145 }
1146
1147 game_ended_with_error(
1148 make_stringf("No random monsters for place '%s'",
1149 arena::place.describe().c_str()));
1150 }
1151
arena_veto_random_monster(monster_type type)1152 bool arena_veto_random_monster(monster_type type)
1153 {
1154 if (mons_is_tentacle_or_tentacle_segment(type))
1155 return true;
1156 if (!arena::allow_immobile && mons_class_is_stationary(type))
1157 return true;
1158 if (!arena::allow_zero_xp && !mons_class_gives_xp(type))
1159 return true;
1160 if (!(mons_char(type) & ~127) && arena::banned_glyphs[mons_char(type)])
1161 return true;
1162
1163 return false;
1164 }
1165
arena_veto_place_monster(const mgen_data & mg,bool first_band_member,const coord_def & pos)1166 bool arena_veto_place_monster(const mgen_data &mg, bool first_band_member,
1167 const coord_def& pos)
1168 {
1169 UNUSED(pos);
1170
1171 // If the first band member makes it past the summon throttle cut,
1172 // let all of the rest of its band in too regardless of the summon
1173 // throttle.
1174 if (mg.abjuration_duration > 0 && first_band_member)
1175 {
1176 if (mg.behaviour == BEH_FRIENDLY
1177 && arena::faction_a.active_members > arena::summon_throttle)
1178 {
1179 return true;
1180 }
1181 else if (mg.behaviour == BEH_HOSTILE
1182 && arena::faction_b.active_members > arena::summon_throttle)
1183 {
1184 return true;
1185 }
1186
1187 }
1188 return !arena::allow_bands && !first_band_member
1189 || !(mons_char(mg.cls) & ~127)
1190 && arena::banned_glyphs[mons_char(mg.cls)];
1191 }
1192
1193 // XXX: Still having some trouble with book-keeping if a slime creature
1194 // is placed via splitting.
arena_placed_monster(monster * mons)1195 void arena_placed_monster(monster* mons)
1196 {
1197 if (mons_is_tentacle_or_tentacle_segment(mons->type))
1198 ; // we don't count tentacles or tentacle segments, even free-standing
1199 else if (mons->attitude == ATT_FRIENDLY)
1200 {
1201 arena::faction_a.active_members++;
1202 arena::faction_b.won = false;
1203 }
1204 else if (mons->attitude == ATT_HOSTILE)
1205 {
1206 arena::faction_b.active_members++;
1207 arena::faction_a.won = false;
1208 }
1209
1210 if (!arena::allow_summons || !arena::allow_animate)
1211 {
1212 arena::adjust_spells(mons, !arena::allow_summons,
1213 !arena::allow_animate);
1214 }
1215
1216 if (mons->type == MONS_TEST_SPAWNER)
1217 {
1218 if (mons->attitude == ATT_FRIENDLY)
1219 arena::a_spawners.push_back(mons->mindex());
1220 else if (mons->attitude == ATT_HOSTILE)
1221 arena::b_spawners.push_back(mons->mindex());
1222 }
1223
1224 const bool summoned = mons->is_summoned();
1225
1226 #ifdef ARENA_VERBOSE
1227 mprf("%s %s!",
1228 mons->full_name(DESC_A).c_str(),
1229 arena::is_respawning ? "respawns" :
1230 (summoned && ! arena::real_summons) ? "is summoned"
1231 : "enters the arena");
1232 #endif
1233
1234 for (mon_inv_iterator ii(*mons); ii; ++ii)
1235 {
1236 ii->flags |= ISFLAG_IDENT_MASK;
1237
1238 // Set the "drop" time here in case the monster drops the
1239 // item without dying, like being polymorphed.
1240 arena::item_drop_times[ii->index()] = arena::turns;
1241 }
1242
1243 if (arena::name_monsters && !mons->is_named())
1244 mons->mname = make_name();
1245
1246 if (summoned)
1247 {
1248 // Real summons drop corpses and items.
1249 if (arena::real_summons)
1250 {
1251 mons->del_ench(ENCH_ABJ, true, false);
1252 for (mon_inv_iterator ii(*mons); ii; ++ii)
1253 ii->flags &= ~ISFLAG_SUMMONED;
1254 }
1255
1256 if (arena::move_summons)
1257 monster_teleport(mons, true, true);
1258
1259 if (!arena::allow_chain_summons)
1260 arena::adjust_spells(mons, true, false);
1261 }
1262 }
1263
1264 // Take care of respawning slime creatures merging and then splitting.
arena_split_monster(monster * split_from,monster * split_to)1265 void arena_split_monster(monster* split_from, monster* split_to)
1266 {
1267 if (!arena::respawn)
1268 return;
1269
1270 const int from_idx = split_from->mindex();
1271 const int member_idx = arena::to_respawn[from_idx];
1272
1273 if (member_idx == -1)
1274 return;
1275
1276 arena::to_respawn[split_to->mindex()] = member_idx;
1277 }
1278
arena_monster_died(monster * mons,killer_type killer,int killer_index,bool silent,const item_def * corpse)1279 void arena_monster_died(monster* mons, killer_type killer,
1280 int killer_index, bool silent, const item_def* corpse)
1281 {
1282 if (mons_is_tentacle_or_tentacle_segment(mons->type))
1283 ; // part of a monster, or a spell
1284 else if (mons->attitude == ATT_FRIENDLY)
1285 arena::faction_a.active_members--;
1286 else if (mons->attitude == ATT_HOSTILE)
1287 arena::faction_b.active_members--;
1288
1289 if (arena::faction_a.active_members > 0
1290 && arena::faction_b.active_members <= 0)
1291 {
1292 arena::faction_a.won = true;
1293 }
1294 else if (arena::faction_b.active_members > 0
1295 && arena::faction_a.active_members <= 0)
1296 {
1297 arena::faction_b.won = true;
1298 }
1299 // Everyone is dead. Is it a tie, or something else?
1300 else if (arena::faction_a.active_members <= 0
1301 && arena::faction_b.active_members <= 0)
1302 {
1303 if (mons->flags & MF_HARD_RESET && !MON_KILL(killer))
1304 mpr("Last arena monster was dismissed.");
1305 // If all monsters are dead, and the last one to die is a giant
1306 // spore or ball lightning, then that monster's faction is the
1307 // winner, since self-destruction is their purpose. But if a
1308 // trap causes the spore to explode, and that kills everything,
1309 // it's a tie, since it counts as the trap killing everyone.
1310 else if (mons_self_destructs(*mons) && MON_KILL(killer))
1311 {
1312 if (mons->attitude == ATT_FRIENDLY)
1313 arena::faction_a.won = true;
1314 else if (mons->attitude == ATT_HOSTILE)
1315 arena::faction_b.won = true;
1316 }
1317 }
1318
1319 // Only respawn those monsters which were initially placed in the
1320 // arena.
1321 const int midx = mons->mindex();
1322 if (arena::respawn && arena::to_respawn[midx] != -1
1323 // Don't respawn when a slime 'dies' from merging with another
1324 // slime.
1325 && !(mons->type == MONS_SLIME_CREATURE && silent
1326 && killer == KILL_MISC
1327 && killer_index == NON_MONSTER))
1328 {
1329 arena::faction *fac = nullptr;
1330 if (mons->attitude == ATT_FRIENDLY)
1331 fac = &arena::faction_a;
1332 else if (mons->attitude == ATT_HOSTILE)
1333 fac = &arena::faction_b;
1334
1335 if (fac)
1336 {
1337 int member_idx = arena::to_respawn[midx];
1338 fac->respawn_list.push_back(member_idx);
1339 fac->respawn_pos.push_back(mons->pos());
1340
1341 // Un-merge slime when it respawns, but only if it's
1342 // specifically a slime, and not a random monster which
1343 // happens to be a slime.
1344 if (mons->type == MONS_SLIME_CREATURE
1345 && (fac->members.get_monster(member_idx).type
1346 == MONS_SLIME_CREATURE))
1347 {
1348 for (int i = 1; i < mons->blob_size; i++)
1349 {
1350 fac->respawn_list.push_back(member_idx);
1351 fac->respawn_pos.push_back(mons->pos());
1352 }
1353 }
1354
1355 arena::to_respawn[midx] = -1;
1356 }
1357 }
1358
1359 if (corpse)
1360 arena::item_drop_times[corpse->index()] = arena::turns;
1361
1362 // Won't be dropping any items.
1363 if (mons->flags & MF_HARD_RESET)
1364 return;
1365
1366 for (mon_inv_iterator ii(*mons); ii; ++ii)
1367 {
1368 if (ii->flags & ISFLAG_SUMMONED)
1369 continue;
1370
1371 arena::item_drop_times[ii->index()] = arena::turns;
1372 }
1373 }
1374
_sort_by_age(int a,int b)1375 static bool _sort_by_age(int a, int b)
1376 {
1377 return arena::item_drop_times[a] < arena::item_drop_times[b];
1378 }
1379
1380 #define DESTROY_ITEM(i) \
1381 { \
1382 destroy_item(i, true); \
1383 arena::item_drop_times[i] = 0; \
1384 cull_count++; \
1385 if (first_avail == NON_ITEM) \
1386 first_avail = i; \
1387 }
1388
1389 // Culls the items which have been on the floor the longest, culling the
1390 // newest items last. Items which a monster dropped voluntarily or
1391 // because of being polymorphed, rather than because of dying, are
1392 // culled earlier than they should be, but it's not like we have to be
1393 // fair to the arena monsters.
arena_cull_items()1394 int arena_cull_items()
1395 {
1396 vector<int> items;
1397
1398 int first_avail = NON_ITEM;
1399
1400 for (int i = 0; i < MAX_ITEMS; i++)
1401 {
1402 // All items in env.item[] are valid when we're called.
1403 const item_def &item(env.item[i]);
1404
1405 // We want floor items.
1406 if (!in_bounds(item.pos))
1407 continue;
1408
1409 items.push_back(i);
1410 }
1411
1412 // Cull half of items on the floor.
1413 const int cull_target = items.size() / 2;
1414 int cull_count = 0;
1415
1416 sort(items.begin(), items.end(), _sort_by_age);
1417
1418 vector<int> ammo;
1419
1420 for (int idx : items)
1421 {
1422 const item_def &item(env.item[idx]);
1423
1424 // If the drop time is 0 then this is probably thrown ammo.
1425 if (arena::item_drop_times[idx] == 0)
1426 {
1427 // We know it's at least this old.
1428 arena::item_drop_times[idx] = arena::turns;
1429
1430 // Arrows/needles/etc on the floor is just clutter.
1431 if (item.base_type != OBJ_MISSILES
1432 || item.sub_type == MI_JAVELIN
1433 || item.sub_type == MI_BOOMERANG
1434 || item.sub_type == MI_THROWING_NET)
1435 {
1436 ammo.push_back(idx);
1437 continue;
1438 }
1439 }
1440 DESTROY_ITEM(idx);
1441 if (cull_count >= cull_target)
1442 break;
1443 }
1444
1445 if (cull_count >= cull_target)
1446 {
1447 dprf("On turn #%d culled %d items dropped by monsters, done.",
1448 arena::turns, cull_count);
1449 return first_avail;
1450 }
1451
1452 dprf("On turn #%d culled %d items dropped by monsters, culling some more.",
1453 arena::turns, cull_count);
1454
1455 #ifdef DEBUG_DIAGNOSTICS
1456 const int count1 = cull_count;
1457 #endif
1458 for (int idx : ammo)
1459 {
1460 DESTROY_ITEM(idx);
1461 if (cull_count >= cull_target)
1462 break;
1463 }
1464
1465 if (cull_count >= cull_target)
1466 dprf("Culled %d (probably) ammo items, done.", cull_count - count1);
1467 else
1468 {
1469 dprf("Culled %d items total, short of target %d.",
1470 cull_count, cull_target);
1471 }
1472 return first_avail;
1473 } // arena_cull_items
1474
1475 /////////////////////////////////////////////////////////////////////////////
1476
_init_arena()1477 static void _init_arena()
1478 {
1479 initialise_branch_depths();
1480 run_map_global_preludes();
1481 run_map_local_preludes();
1482 initialise_item_descriptions();
1483 }
1484
_choose_arena_teams(newgame_def & choice,const string & default_arena_teams)1485 static void _choose_arena_teams(newgame_def& choice,
1486 const string &default_arena_teams)
1487 {
1488 #ifdef USE_TILE_WEB
1489 tiles_crt_popup show_as_popup;
1490 #endif
1491
1492 if (!choice.arena_teams.empty())
1493 return;
1494 arena::skipped_arena_ui = false;
1495 clear_message_store();
1496
1497 auto vbox = make_shared<Box>(ui::Widget::VERT);
1498 vbox->add_child(make_shared<Text>("Enter your choice of teams:\n "));
1499 vbox->set_cross_alignment(Widget::Align::STRETCH);
1500 auto teams_input = make_shared<ui::TextEntry>();
1501 teams_input->set_sync_id("teams");
1502 teams_input->set_text(default_arena_teams);
1503 vbox->add_child(teams_input);
1504 formatted_string prompt;
1505 prompt.cprintf("\nExamples:\n");
1506 prompt.cprintf(" Sigmund v Jessica\n");
1507 prompt.cprintf(" 99 orc v the Royal Jelly\n");
1508 prompt.cprintf(" 20-headed hydra v 10 kobold ; scimitar ego:flaming");
1509 vbox->add_child(make_shared<Text>(move(prompt)));
1510
1511 auto popup = make_shared<ui::Popup>(move(vbox));
1512
1513 bool done = false, cancel = false;
1514 popup->on_hotkey_event([&](const KeyEvent& ev) {
1515 return done = (ev.key() == CK_ENTER);
1516 });
1517 popup->on_keydown_event([&](const KeyEvent& ev) {
1518 return done = cancel = key_is_escape(ev.key());
1519 });
1520
1521 ui::run_layout(move(popup), done, teams_input);
1522
1523 if (cancel || crawl_state.seen_hups)
1524 {
1525 arena::global_shutdown();
1526 game_ended(crawl_state.bypassed_startup_menu
1527 ? game_exit::death : game_exit::abort);
1528 }
1529 choice.arena_teams = teams_input->get_text();
1530 if (choice.arena_teams.empty())
1531 choice.arena_teams = default_arena_teams;
1532 }
1533
run_arena(const newgame_def & choice,const string & default_arena_teams)1534 NORETURN void run_arena(const newgame_def& choice, const string &default_arena_teams)
1535 {
1536 ASSERT(crawl_state.game_is_arena());
1537
1538 newgame_def arena_choice = choice;
1539 string last_teams = default_arena_teams;
1540 if (arena::file != nullptr)
1541 end(0, false, "Results file already open");
1542 // would be more elegant if arena_tee handled file open/close, but
1543 // that would need a bunch of refactoring of how the file is handled here.
1544 arena::file = fopen("arena.result", "w");
1545 msg::arena_tee log(&arena::file);
1546
1547 do
1548 {
1549 try
1550 {
1551 _choose_arena_teams(arena_choice, last_teams);
1552 write_newgame_options_file(arena_choice);
1553 _init_arena();
1554
1555 ASSERT(!crawl_state.arena_suspended);
1556
1557 #ifdef WIZARD
1558 // The player has wizard powers for the duration of the arena.
1559 unwind_bool wiz(you.wizard, true);
1560 #endif
1561
1562 arena::global_setup(arena_choice.arena_teams);
1563 arena::simulate();
1564 arena::global_shutdown();
1565 game_ended(game_exit::death); // there is only death in the arena
1566 }
1567 catch (const arena::arena_error &error)
1568 {
1569 if (error.fatal || arena::skipped_arena_ui)
1570 {
1571 arena::write_error(error.what());
1572 game_ended_with_error(error.what());
1573 }
1574 else
1575 {
1576 mprf(MSGCH_ERROR, "%s", error.what());
1577 _results_popup(error.what(), true);
1578 last_teams = arena_choice.arena_teams;
1579 arena_choice.arena_teams = "";
1580 // fallthrough
1581 }
1582 }
1583 }
1584 while (true);
1585 }
1586