1 /* Management of historical information about an Xconq game.
2 Copyright (C) 1992-2000 Stanley T. Shebs.
3
4 Xconq is free software; you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation; either version 2, or (at your option)
7 any later version. See the file COPYING. */
8
9 #include "conq.h"
10
11 static void notify_event(Side *side, HistEvent *hevt);
12 static void play_event_movies(Side *side, HistEvent *hevt);
13 static void rewrite_unit_references(int oldid, int newid);
14 static void rewrite_unit_references_in_event(HistEvent *hevt, int oldid,
15 int newid);
16
17 extern int any_post_event_scores;
18 extern int need_post_event_scores;
19
20 HevtDefn hevtdefns[] = {
21
22 #undef DEF_HEVT
23 #define DEF_HEVT(NAME, code, DATADESCS) { NAME, DATADESCS },
24
25 #include "history.def"
26
27 { NULL, NULL }
28 };
29
30 /* Head of the list of events. */
31
32 HistEvent *history;
33
34 int tmphevtdata1;
35
36 /* The list of all past unit records. */
37
38 PastUnit *past_unit_list;
39
40 PastUnit *last_past_unit;
41
42 /* The id of the next past unit to be synthesized. -1 is reserved, so
43 start counting down from -2. */
44
45 int next_past_unit_id = -2;
46
47 /* Buffers for descriptions of past units. */
48
49 #define NUMPASTBUFS 3
50
51 int curpastbuf = 0;
52
53 char *pastbufs[NUMPASTBUFS] = { NULL, NULL, NULL };
54
55 /* True if events are being recorded into the history. */
56
57 static int recording_events;
58
59 /* True if statistics dump is wanted. */
60
61 int statistics_wanted = FALSE;
62
63 /* True after the statistics file has been written. */
64
65 int statistics_dumped;
66
67 void
init_history(void)68 init_history(void)
69 {
70 /* The first "event" is just a marker. */
71 history = create_historical_event(H_LOG_HEAD);
72 /* Give it an impossible date. */
73 history->startdate = -1;
74 history->next = history->prev = history;
75 /* Initialize the past unit record. */
76 past_unit_list = last_past_unit = NULL;
77 next_past_unit_id = -2;
78 }
79
80 void
start_history(void)81 start_history(void)
82 {
83 /* Ignore multiple starts. */
84 if (recording_events)
85 return;
86 recording_events = TRUE;
87 record_event(H_LOG_STARTED, ALLSIDES);
88 }
89
90 HistEvent *
create_historical_event(HistEventType type)91 create_historical_event(HistEventType type)
92 {
93 HistEvent *hevt = (HistEvent *) xmalloc(sizeof(HistEvent));
94
95 if (type >= NUMHEVTTYPES)
96 run_warning("unknown hist event type %d", type);
97 hevt->type = type;
98 hevt->observers = ALLSIDES;
99 hevt->next = hevt->prev = NULL;
100 return hevt;
101 }
102
103 /* Record a historical event. */
104
105 HistEvent *
record_event(HistEventType type,SideMask observers,...)106 record_event(HistEventType type, SideMask observers, ...)
107 {
108 int i, val;
109 char *descs;
110 HistEvent *hevt;
111 Side *side;
112 va_list ap;
113
114 if (!recording_events)
115 return NULL;
116 hevt = create_historical_event(type);
117 hevt->startdate = g_turn();
118 hevt->enddate = g_turn();
119 hevt->observers = observers;
120 descs = hevtdefns[type].datadescs;
121
122 va_start(ap, observers);
123 for (i = 0; descs[i] != '\0'; ++i) {
124 if (i >= 4)
125 run_error("hevt type %d has too many parameters", type);
126 val = va_arg(ap, int);
127 hevt->data[i] = val;
128 }
129 va_end(ap);
130
131 /* Check on plausibility of event data. */
132 for (i = 0; descs[i] != '\0'; ++i) {
133 val = hevt->data[i];
134 switch (descs[i]) {
135 case 'S':
136 if (!between(0, val, numsides))
137 run_warning("invalid side number %d in hist event", val);
138 break;
139 case 'U':
140 /* Note that when validating unit id, the unit may be in
141 the middle of the the process of becoming a past unit,
142 so allowing finding a dead unit. */
143 if (val != 0
144 && find_unit_dead_or_alive(val) == NULL
145 && find_past_unit(val) == NULL)
146 run_warning("invalid unit/pastunit id %d in hist event", val);
147 break;
148 default:
149 break;
150 }
151 }
152 /* Insert the newly created event. */
153 hevt->next = history;
154 hevt->prev = history->prev;
155 history->prev->next = hevt;
156 history->prev = hevt;
157 Dprintf("Recorded event %s (observed by %d)\n",
158 hevtdefns[hevt->type].name, hevt->observers);
159 if (observers != NOSIDES) {
160 /* Let all the observers' interfaces look at this event. */
161 for_all_sides(side) {
162 if (side_in_set(side, observers) && active_display(side)) {
163 update_event_display(side, hevt, TRUE);
164 notify_event(side, hevt);
165 if (g_event_movies() != lispnil) {
166 play_event_movies(side, hevt);
167 }
168 }
169 }
170 }
171 /* Flag that we should look at post-event scorekeepers soon. */
172 if (any_post_event_scores && !beforestart && !endofgame)
173 need_post_event_scores = TRUE;
174 return hevt;
175 }
176
177 static void
notify_event(Side * side,HistEvent * hevt)178 notify_event(Side *side, HistEvent *hevt)
179 {
180 char abuf[BUFSIZE];
181 Obj *rest, *head, *pattern, *text;
182 Side *side2;
183 Unit *unit;
184
185 for_all_list(g_event_notices(), rest) {
186 head = car(rest);
187 if (consp(head)) {
188 pattern = car(head);
189 if (symbolp(pattern)
190 && find_event_type(pattern) == hevt->type) {
191 text = cadr(head);
192 if (stringp(text)) {
193 sprintf(abuf, "%s", c_string(text));
194 } else {
195 sprintlisp(abuf, text, 50);
196 }
197 notify(side, "%s", abuf);
198 return;
199 } else if (consp(pattern)
200 && symbolp(car(pattern))
201 && pattern_matches_event(pattern, hevt)
202 ) {
203 text = cadr(head);
204 if (stringp(text)) {
205 strcpy(abuf, c_string(text));
206 } else {
207 event_desc_from_list(side, text, hevt, abuf);
208 }
209 notify(side, "%s", abuf);
210 return;
211 }
212 }
213 }
214 /* If we didn't find anything special, still put out some generic
215 messages for important cases. */
216 switch (hevt->type) {
217 case H_SIDE_LOST:
218 if (hevt->data[0] == side_number(side)) {
219 notify(side, "You lost!");
220 } else {
221 notify(side, "%s lost!", side_desig(side_n(hevt->data[0])));
222 }
223 break;
224 case H_SIDE_WON:
225 if (hevt->data[0] == side_number(side)) {
226 notify(side, "You won!");
227 } else {
228 notify(side, "%s won!", side_desig(side_n(hevt->data[0])));
229 }
230 break;
231 #if 0 /* this already has a special message */
232 case H_GAME_ENDED:
233 notify(side, "The game is over!");
234 break;
235 #endif
236 case H_UNIT_COMPLETED:
237 side2 = side_n(hevt->data[0]);
238 unit = find_unit(hevt->data[1]);
239 if (unit != NULL) {
240 if (side2 == side) {
241 notify(side, "You completed %s.",
242 unit_handle(side, unit));
243 } else {
244 notify(side, "%s completed %s.",
245 side_desig(side2), unit_handle(side2, unit));
246 }
247 }
248 break;
249 case H_UNIT_CREATED:
250 side2 = side_n(hevt->data[0]);
251 unit = find_unit(hevt->data[1]);
252 if (unit != NULL) {
253 if (side2 == side) {
254 notify(side, "You created %s.",
255 unit_handle(side, unit));
256 } else {
257 notify(side, "%s created %s.",
258 side_desig(side2), unit_handle(side2, unit));
259 }
260 }
261 break;
262 default:
263 break;
264 }
265 }
266
267 static void
play_event_movies(Side * side,HistEvent * hevt)268 play_event_movies(Side *side, HistEvent *hevt)
269 {
270 int found = FALSE;
271 char *soundname;
272 Obj *rest, *head, *parms, *msgdesc;
273
274 if (!should_play_movies())
275 return;
276 for_all_list(g_event_movies(), rest) {
277 head = car(rest);
278 if (consp(head)
279 && symbolp(car(head))
280 && hevt->type == find_event_type(car(head))) {
281 found = TRUE;
282 break;
283 }
284 if (consp(head)
285 && consp(car(head))
286 && symbolp(car(car(head)))
287 && hevt->type == find_event_type(car(car(head)))) {
288 parms = cdr(car(head));
289 if (parms == lispnil) {
290 found = TRUE;
291 break;
292 }
293 #if 0
294 if (((symbolp(car(parms))
295 && strcmp(c_string(car(parms)),
296 u_type_name(unit->type)) == 0)
297 || match_keyword(car(parms), K_USTAR)
298 || (symbolp(car(parms))
299 && boundp(car(parms))
300 && ((symbolp(symbol_value(car(parms)))
301 && strcmp(c_string(symbol_value(car(parms))),
302 u_type_name(unit->type)) == 0)
303 || (numberp(symbol_value(car(parms)))
304 && c_number(symbol_value(car(parms)))
305 == unit->type)))
306 )) {
307 found = TRUE;
308 break;
309 }
310 /* (should be able to match on particular data also) */
311 #endif
312 }
313 }
314 /* If we have a match, do something with it. */
315 if (found) {
316 msgdesc = cadr(head);
317 if (stringp(msgdesc)) {
318 notify(side, "%s", c_string(msgdesc));
319 } else if (consp(msgdesc)
320 && symbolp(car(msgdesc))
321 && strcmp(c_string(car(msgdesc)), "sound") == 0
322 && stringp(cadr(msgdesc))) {
323 soundname = c_string(cadr(msgdesc));
324 /* (should not be passing ptrs to schedule_movie) */
325 schedule_movie(side, "sound", soundname);
326 play_movies(add_side_to_set(side, NOSIDES));
327 } else {
328 }
329 }
330 }
331
332 /* A unit's death requires some bookkeeping - we need to update the
333 history to use a past unit, plus record the loss for statistics
334 purposes. */
335
336 void
record_unit_death(Unit * unit,HistEventType reason)337 record_unit_death(Unit *unit, HistEventType reason)
338 {
339 enum loss_reasons lossreason;
340 PastUnit *pastunit;
341
342 pastunit = create_past_unit(unit->type, 0);
343 pastunit->name = unit->name;
344 pastunit->number = unit->number;
345 pastunit->x = unit->x; pastunit->y = unit->y; pastunit->z = unit->z;
346 pastunit->side = unit->side;
347 rewrite_unit_references(unit->id, pastunit->id);
348 if (reason >= 0) {
349 switch (reason) {
350 case H_UNIT_GARRISONED:
351 /* Garrison events mention the unit being garrisoned. */
352 record_event(reason, add_side_to_set(unit->side, NOSIDES),
353 pastunit->id, tmphevtdata1);
354 break;
355 default:
356 record_event(reason, add_side_to_set(unit->side, NOSIDES),
357 pastunit->id, -1);
358 break;
359 }
360 }
361 /* Reduce specific events to general categories of unit loss, used
362 for statistics display. */
363 /* Note that we track independent unit losses as well. */
364 switch (reason) {
365 case H_UNIT_KILLED:
366 case H_UNIT_WRECKED:
367 lossreason = combat_loss;
368 break;
369 case H_UNIT_STARVED:
370 lossreason = starvation_loss;
371 break;
372 case H_UNIT_DIED_IN_ACCIDENT:
373 case H_UNIT_WRECKED_IN_ACCIDENT:
374 case H_UNIT_VANISHED:
375 lossreason = accident_loss;
376 break;
377 case H_UNIT_DIED_FROM_ATTRITION:
378 case H_UNIT_WRECKED_FROM_ATTRITION:
379 lossreason = attrition_loss;
380 break;
381 case H_UNIT_DISBANDED:
382 lossreason = disband_loss;
383 break;
384 default:
385 lossreason = other_loss;
386 break;
387 }
388 count_loss(unit->side, unit->type, lossreason);
389 }
390
391 /* Create a past unit corresponding to the given unit, replace unit
392 references in the history. */
393
394 PastUnit *
change_unit_to_past_unit(Unit * unit)395 change_unit_to_past_unit(Unit *unit)
396 {
397 PastUnit *pastunit;
398
399 pastunit = create_past_unit(unit->type, 0);
400 pastunit->name = unit->name;
401 pastunit->number = unit->number;
402 pastunit->x = unit->x; pastunit->y = unit->y; pastunit->z = unit->z;
403 pastunit->side = unit->side;
404 rewrite_unit_references(unit->id, pastunit->id);
405 return pastunit;
406 }
407
408 void
record_unit_side_change(Unit * unit,Side * newside,HistEventType reason,Unit * agent)409 record_unit_side_change(Unit *unit, Side *newside, HistEventType reason,
410 Unit *agent)
411 {
412 int capture;
413 enum loss_reasons lossreason;
414 enum gain_reasons gainreason;
415 SideMask observers;
416 PastUnit *pastunit;
417
418 /* Side loss hevt has no space for individual units, so change to a type
419 of event that does. */
420 if (reason == H_SIDE_LOST)
421 reason = H_UNIT_ACQUIRED;
422 pastunit = change_unit_to_past_unit(unit);
423 observers = NOSIDES;
424 observers = add_side_to_set(unit->side, observers);
425 observers = add_side_to_set(newside, observers);
426 if (agent != NULL)
427 observers = add_side_to_set(agent->side, observers);
428 /* Check the event type to decide how many parameters to pass to
429 the generic recorder. */
430 capture = (reason == H_UNIT_CAPTURED || reason == H_UNIT_SURRENDERED);
431 if (capture)
432 record_event(reason, observers, pastunit->id, (agent ? agent->id : 0),
433 side_number(newside));
434 else
435 record_event(reason, observers, pastunit->id,
436 side_number(newside));
437 lossreason = (capture ? capture_loss : other_loss);
438 count_loss(unit->side, unit->type, lossreason);
439 gainreason = (capture ? capture_gain : other_gain);
440 count_gain(newside, unit->type, gainreason);
441 }
442
443 /* Record a unit's name change by creating a past unit with the
444 previous name. */
445
446 void
record_unit_name_change(Unit * unit,char * newname)447 record_unit_name_change(Unit *unit, char *newname)
448 {
449 PastUnit *pastunit;
450
451 pastunit = change_unit_to_past_unit(unit);
452 record_event(H_UNIT_NAME_CHANGED, ALLSIDES, pastunit->id, unit->id);
453 }
454
455 void
count_gain(Side * side,int u,enum gain_reasons reason)456 count_gain(Side *side, int u, enum gain_reasons reason)
457 {
458 assert_error((reason < num_gain_reasons),
459 "Invalid gain reason given for historical record.");
460 assert_error(is_unit_type(u),
461 "Invalid unit type given for historical record.");
462 if (side)
463 ++(side->gaincounts[num_gain_reasons * u + reason]);
464 }
465
466 void
count_loss(Side * side,int u,enum loss_reasons reason)467 count_loss(Side *side, int u, enum loss_reasons reason)
468 {
469 assert_error((reason < num_loss_reasons),
470 "Invalid loss reason given for historical record.");
471 assert_error(is_unit_type(u),
472 "Invalid unit type given for historical record.");
473 if (side)
474 ++(side->losscounts[num_loss_reasons * u + reason]);
475 }
476
477 /* Create a past unit with the given type and for the unit with
478 the given id. */
479
480 PastUnit *
create_past_unit(int type,int id)481 create_past_unit(int type, int id)
482 {
483 PastUnit *pastunit = (PastUnit *) xmalloc(sizeof(PastUnit));
484
485 pastunit->type = type;
486 if (id == 0)
487 id = next_past_unit_id--;
488 else
489 next_past_unit_id = min(next_past_unit_id, id - 1);
490 pastunit->id = id;
491 /* Glue at the end of the list, so all stays sorted. */
492 if (past_unit_list == NULL) {
493 past_unit_list = last_past_unit = pastunit;
494 } else {
495 last_past_unit->next = pastunit;
496 last_past_unit = pastunit;
497 }
498 return pastunit;
499 }
500
501 PastUnit *
find_past_unit(int n)502 find_past_unit(int n)
503 {
504 PastUnit *pastunit;
505
506 for (pastunit = past_unit_list; pastunit != NULL; pastunit = pastunit->next) {
507 if (pastunit->id == n)
508 return pastunit;
509 }
510 return NULL;
511 }
512
513 /* Scan through the history, changing matching unit/pastunit
514 references into past unit references. */
515
516 static void
rewrite_unit_references(int oldid,int newid)517 rewrite_unit_references(int oldid, int newid)
518 {
519 HistEvent *hevt;
520
521 rewrite_unit_references_in_event(history, oldid, newid);
522 for (hevt = history->next; hevt != history; hevt = hevt->next) {
523 rewrite_unit_references_in_event(hevt, oldid, newid);
524 }
525 }
526
527 static void
rewrite_unit_references_in_event(HistEvent * hevt,int oldid,int newid)528 rewrite_unit_references_in_event(HistEvent *hevt, int oldid, int newid)
529 {
530 int i;
531 char *descs;
532
533 descs = hevtdefns[hevt->type].datadescs;
534 /* Scan through the data description, looking for values that are
535 references to actual units. */
536 for (i = 0; descs[i] != '\0'; ++i) {
537 if (descs[i] == 'U' && hevt->data[i] == oldid)
538 hevt->data[i] = newid;
539 }
540 }
541
542 /* Indicate that history is no longer being recorded. */
543
544 void
end_history(void)545 end_history(void)
546 {
547 record_event(H_LOG_ENDED, ALLSIDES);
548 recording_events = FALSE;
549 }
550
551 /* Find the historical event that would be at the given index, in
552 a list where each event gets one line, and dates appear on
553 separate lines whenever the date is different. */
554
555 HistEvent *
get_nth_history_line(Side * side,int n,HistEvent ** nextevt)556 get_nth_history_line(Side *side, int n, HistEvent **nextevt)
557 {
558 int i = 0;
559 HistEvent *hevt, *hevtprev = NULL;
560 char buf[500];
561
562 Dprintf("Getting line %d\n", n);
563 for (hevt = history->next; hevt != history; hevt = hevt->next) {
564 historical_event_desc(side, hevt, buf);
565 Dprintf(" event is %s, date is %d, i is %d\n", buf, hevt->startdate, i);
566 if (side_in_set(side, hevt->observers)) {
567 Dprintf(" observed\n");
568 if (hevtprev == NULL || hevt->startdate != hevtprev->startdate) {
569 if (i == n) {
570 Dprintf("Returning NULL\n");
571 return NULL;
572 }
573 ++i;
574 }
575 if (i == n) {
576 historical_event_desc(side, hevt, buf);
577 Dprintf("Returning %s\n", hevt);
578 return hevt;
579 }
580 ++i;
581 hevtprev = hevt;
582 }
583 }
584 /* Return the last event. */
585 return history->prev;
586 }
587
588 /* Count the total number of lines that can be in the history. */
589
590 int
update_total_hist_lines(Side * side)591 update_total_hist_lines(Side *side)
592 {
593 int nlines;
594 HistEvent *hevt, *hevtprev = NULL;
595
596 nlines = 0;
597 for (hevt = history->next; hevt != history; hevt = hevt->next) {
598 if (side_in_set(side, hevt->observers)) {
599 /* Count an extra line for date changes. */
600 if (hevtprev == NULL || hevt->startdate != hevtprev->startdate)
601 ++nlines;
602 ++nlines;
603 hevtprev = hevt;
604 }
605 }
606 return nlines;
607 }
608
609 /* This routine builds up the array of events and dates to draw, with
610 a combination of pointers to hevts and NULLs where dates should
611 appear. */
612
613 int
build_hist_contents(Side * side,int n,HistEvent ** histcontents,int numvishistlines)614 build_hist_contents(Side *side, int n, HistEvent **histcontents,
615 int numvishistlines)
616 {
617 int numcontents;
618 HistEvent *hevt, *nexthevt;
619
620 numcontents = 0;
621 hevt = get_nth_history_line(side, n, &nexthevt);
622 histcontents[numcontents++] = hevt;
623 /* Since we want to iterate over links (get_nth_history_line may not
624 be very efficient), ensure that we have an actual hevt. */
625 if (hevt == NULL) {
626 hevt = get_nth_history_line(side, n + 1, &nexthevt);
627 histcontents[numcontents++] = hevt;
628 }
629 for (hevt = hevt->next; hevt != history; hevt = hevt->next) {
630 /* Stop if we've filled the contents buffer. */
631 if (numcontents >= numvishistlines)
632 break;
633 if (side_in_set(side, hevt->observers)) {
634 /* Leave a NULL if the date of this event is different
635 from the last. */
636 if (numcontents > 0
637 && histcontents[numcontents - 1] != NULL
638 && hevt->startdate != histcontents[numcontents - 1]->startdate) {
639 histcontents[numcontents++] = NULL;
640 }
641 histcontents[numcontents++] = hevt;
642 }
643 }
644 return numcontents;
645 }
646
647 /* Summarize various aspects of performance in the game, writing it
648 all to a file. */
649
650 void
dump_statistics(void)651 dump_statistics(void)
652 {
653 char *fname;
654 Side *side;
655 FILE *fp;
656
657 if (!statistics_wanted)
658 return;
659 fname = statistics_filename();
660 /* If no name returned, assume that there is a good reason
661 (for instance, the user might have cancelled). */
662 if (empty_string(fname))
663 return;
664 fp = open_file(fname, "w");
665 if (fp != NULL) {
666 if (1 /* records exist */) {
667 write_side_results(fp, NULL);
668 write_unit_record(fp, NULL);
669 write_combat_results(fp, NULL);
670 fprintf(fp, "\f\n");
671 for_all_sides(side) {
672 write_side_results(fp, side);
673 write_unit_record(fp, side);
674 write_combat_results(fp, side);
675 if (side->next != NULL)
676 fprintf(fp, "\f\n");
677 }
678 } else {
679 fprintf(fp, "No statistics were kept.\n");
680 }
681 statistics_dumped = TRUE;
682 fclose(fp);
683 } else {
684 run_warning("Can't open statistics file \"%s\"", fname);
685 }
686 }
687
688 /* Short, unreadable, but greppable listing of past unit. Primarily
689 useful for debugging and warnings. We use several buffers and
690 rotate between them so we can call this more than once in a single
691 printf. */
692
693 char *
past_unit_desig(PastUnit * pastunit)694 past_unit_desig(PastUnit *pastunit)
695 {
696 char *shortbuf;
697
698 if (pastunit == NULL)
699 return "no pastunit";
700 /* Allocate if not yet done so. */
701 if (pastbufs[curpastbuf] == NULL)
702 pastbufs[curpastbuf] = (char *)xmalloc(BUFSIZE);
703 shortbuf = pastbufs[curpastbuf];
704 curpastbuf = (curpastbuf + 1) % NUMPASTBUFS;
705 if (pastunit->id == -1) {
706 sprintf(shortbuf, "s%d head", side_number(pastunit->side));
707 return shortbuf;
708 } else if (is_unit_type(pastunit->type)) {
709 sprintf(shortbuf, "s%d %-3.3s %d (%d,%d",
710 side_number(pastunit->side),
711 shortest_unique_name(pastunit->type),
712 pastunit->id, pastunit->x, pastunit->y);
713 if (pastunit->z != 0)
714 tprintf(shortbuf, ",%d", pastunit->z);
715 strcat(shortbuf, ")"); /* close out the pastunit location */
716 return shortbuf;
717 } else {
718 return "!garbage pastunit!";
719 }
720 }
721
722 /* Would be faster to stash these, but enough difference to care? */
723
724 int
total_gain(Side * side,int u)725 total_gain(Side *side, int u)
726 {
727 int i, total = 0;
728
729 for (i = 0; i < num_gain_reasons; ++i)
730 total += side_gain_count(side, u, i);
731 return total;
732 }
733
734 int
total_loss(Side * side,int u)735 total_loss(Side *side, int u)
736 {
737 int i, total = 0;
738
739 for (i = 0; i < num_loss_reasons; ++i)
740 total += side_loss_count(side, u, i);
741 return total;
742 }
743