Transform default game/locale data paths based on executable name
[neverball] / ball / demo.c
1 /*
2  * Copyright (C) 2003 Robert Kooima
3  *
4  * NEVERBALL is  free software; you can redistribute  it and/or modify
5  * it under the  terms of the GNU General  Public License as published
6  * by the Free  Software Foundation; either version 2  of the License,
7  * or (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful, but
10  * WITHOUT  ANY  WARRANTY;  without   even  the  implied  warranty  of
11  * MERCHANTABILITY or  FITNESS FOR A PARTICULAR PURPOSE.   See the GNU
12  * General Public License for more details.
13  */
14
15 #include <stdio.h>
16 #include <stdlib.h>
17 #include <string.h>
18 #include <time.h>
19 #include <assert.h>
20
21 #include "demo.h"
22 #include "audio.h"
23 #include "solid.h"
24 #include "config.h"
25 #include "binary.h"
26 #include "text.h"
27 #include "common.h"
28 #include "level.h"
29
30 #include "game_server.h"
31 #include "game_client.h"
32 #include "game_proxy.h"
33
34 /*---------------------------------------------------------------------------*/
35
36 #define MAGIC           0x52424EAF
37 #define DEMO_VERSION    9
38
39 #define DATELEN 20
40
41 static FILE *demo_fp;
42
43 static struct demo demos[MAXDEMO]; /* Array of scanned demos  */
44 static int         count;          /* Number of scanned demos */
45
46 /*---------------------------------------------------------------------------*/
47
48 void demo_dump_info(const struct demo *d)
49 {
50     printf("Name:         %s\n"
51            "File:         %s\n"
52            "Time:         %d\n"
53            "Coins:        %d\n"
54            "Mode:         %d\n"
55            "State:        %d\n"
56            "Date:         %s"
57            "Player:       %s\n"
58            "Shot:         %s\n"
59            "Level:        %s\n"
60            "Time:         %d\n"
61            "Goal:         %d\n"
62            "Goal enabled: %d\n"
63            "Score:        %d\n"
64            "Balls:        %d\n"
65            "Total Time:   %d\n",
66            d->name, d->filename,
67            d->timer, d->coins, d->mode, d->status, ctime(&d->date),
68            d->player,
69            d->shot, d->file,
70            d->time, d->goal, d->goal_e, d->score, d->balls, d->times);
71 }
72
73 static int demo_header_read(FILE *fp, struct demo *d)
74 {
75     int magic;
76     int version;
77     int t;
78
79     struct tm date;
80     char datestr[DATELEN];
81
82     get_index(fp, &magic);
83     get_index(fp, &version);
84
85     get_index(fp, &t);
86
87     if (magic == MAGIC && version == DEMO_VERSION && t)
88     {
89         d->timer = t;
90
91         get_index(fp, &d->coins);
92         get_index(fp, &d->status);
93         get_index(fp, &d->mode);
94
95         get_string(fp, d->player, MAXNAM);
96         get_string(fp, datestr, DATELEN);
97
98         sscanf(datestr,
99                "%d-%d-%dT%d:%d:%d",
100                &date.tm_year,
101                &date.tm_mon,
102                &date.tm_mday,
103                &date.tm_hour,
104                &date.tm_min,
105                &date.tm_sec);
106
107         date.tm_year -= 1900;
108         date.tm_mon  -= 1;
109         date.tm_isdst = -1;
110
111         d->date = make_time_from_utc(&date);
112
113         get_string(fp, d->shot, PATHMAX);
114         get_string(fp, d->file, PATHMAX);
115
116         get_index(fp, &d->time);
117         get_index(fp, &d->goal);
118         get_index(fp, &d->goal_e);
119         get_index(fp, &d->score);
120         get_index(fp, &d->balls);
121         get_index(fp, &d->times);
122
123         return 1;
124     }
125     return 0;
126 }
127
128 static void demo_header_write(FILE *fp, struct demo *d)
129 {
130     int magic = MAGIC;
131     int version = DEMO_VERSION;
132     int zero  = 0;
133
134     char datestr[DATELEN];
135
136     strftime(datestr, DATELEN, "%Y-%m-%dT%H:%M:%S", gmtime(&d->date));
137
138     put_index(fp, &magic);
139     put_index(fp, &version);
140     put_index(fp, &zero);
141     put_index(fp, &zero);
142     put_index(fp, &zero);
143     put_index(fp, &d->mode);
144
145     put_string(fp, d->player);
146     put_string(fp, datestr);
147
148     put_string(fp, d->shot);
149     put_string(fp, d->file);
150
151     put_index(fp, &d->time);
152     put_index(fp, &d->goal);
153     put_index(fp, &d->goal_e);
154     put_index(fp, &d->score);
155     put_index(fp, &d->balls);
156     put_index(fp, &d->times);
157 }
158
159 /*---------------------------------------------------------------------------*/
160
161 /* Scan another file (used by demo_scan). */
162
163 static void demo_scan_file(const char *filename)
164 {
165     FILE *fp;
166     struct demo *d = &demos[count];
167
168     if ((fp = fopen(config_user(filename), FMODE_RB)))
169     {
170         if (demo_header_read(fp, d))
171         {
172             strncpy(d->filename, config_user(filename), MAXSTR);
173             strncpy(d->name,
174                     base_name(text_from_locale(d->filename), REPLAY_EXT),
175                     PATHMAX);
176             d->name[PATHMAX - 1] = '\0';
177
178             count++;
179         }
180         fclose(fp);
181     }
182 }
183
184 #ifdef _WIN32
185
186 int demo_scan(void)
187 {
188     WIN32_FIND_DATA d;
189     HANDLE h;
190
191     count = 0;
192
193     /* Scan the user directory for files. */
194
195     if ((h = FindFirstFile(config_user("*"), &d)) != INVALID_HANDLE_VALUE)
196     {
197         do
198             demo_scan_file(d.cFileName);
199         while (count < MAXDEMO && FindNextFile(h, &d));
200
201         FindClose(h);
202     }
203     return count;
204 }
205
206 #else /* _WIN32 */
207 #include <dirent.h>
208
209 int demo_scan(void)
210 {
211     struct dirent *ent;
212     DIR  *dp;
213
214     count = 0;
215
216     /* Scan the user directory for files. */
217
218     if ((dp = opendir(config_user(""))))
219     {
220         while (count < MAXDEMO && (ent = readdir(dp)))
221             demo_scan_file(ent->d_name);
222
223         closedir(dp);
224     }
225     return count;
226 }
227 #endif /* _WIN32 */
228
229 const char *demo_pick(void)
230 {
231     int n = demo_scan();
232
233     return (n > 0) ? demos[(rand() >> 4) % n].filename : NULL;
234 }
235
236 const struct demo *demo_get(int i)
237 {
238     return (0 <= i && i < count) ? &demos[i] : NULL;
239 }
240
241 /*---------------------------------------------------------------------------*/
242
243 int demo_exists(const char *name)
244 {
245     char buf[MAXSTR];
246
247     strcpy(buf, config_user(name));
248     strcat(buf, REPLAY_EXT);
249
250     return file_exists(buf);
251 }
252
253 #define MAXSTRLEN(a) (sizeof ((a)) - 1)
254
255 const char *demo_format_name(const char *fmt,
256                              const char *set,
257                              const char *level)
258 {
259     static char name[MAXSTR];
260     int space_left;
261     char *numpart;
262     int i;
263
264     if (!fmt)
265         return NULL;
266
267     memset(name, 0, sizeof (name));
268     space_left = MAXSTRLEN(name);
269
270     /* Construct name, replacing each format sequence as appropriate. */
271
272     while (*fmt && space_left > 0)
273     {
274         if (*fmt == '%')
275         {
276             fmt++;
277
278             switch (*fmt)
279             {
280             case 's':
281                 if (set)
282                 {
283                     strncat(name, set, space_left);
284                     space_left -= strlen(set);
285                 }
286                 break;
287
288             case 'l':
289                 if (level)
290                 {
291                     strncat(name, level, space_left);
292                     space_left -= strlen(level);
293                 }
294                 break;
295
296             case '%':
297                 strncat(name, "%", space_left);
298                 space_left--;
299                 break;
300
301             case '\0':
302                 fputs(L_("Missing format character in replay name\n"), stderr);
303                 fmt--;
304                 break;
305
306             default:
307                 fprintf(stderr, L_("Invalid format character in "
308                                    "replay name: \"%%%c\"\n"), *fmt);
309                 break;
310             }
311         }
312         else
313         {
314             strncat(name, fmt, 1);
315             space_left--;
316         }
317
318         fmt++;
319     }
320
321     /*
322      * Append a unique 2-digit number preceded by an underscore to the
323      * file name, discarding characters if there's not enough space
324      * left in the buffer.
325      */
326
327     if (space_left < strlen("_23"))
328         numpart = name + MAXSTRLEN(name) - strlen("_23");
329     else
330         numpart = name + MAXSTRLEN(name) - space_left;
331
332     for (i = 1; i < 100; i++)
333     {
334         sprintf(numpart, "_%02d", i);
335
336         if (!demo_exists(name))
337             break;
338     }
339
340     return name;
341 }
342
343 #undef MAXSTRLEN
344
345 /*---------------------------------------------------------------------------*/
346
347 int demo_play_init(const char *name, const struct level *level,
348                    int mode, int t, int g, int e, int s, int b, int tt)
349 {
350     struct demo demo;
351
352     memset(&demo, 0, sizeof (demo));
353
354     strncpy(demo.filename, config_user(name), MAXSTR);
355     strcat(demo.filename, REPLAY_EXT);
356
357     demo.mode = mode;
358     demo.date = time(NULL);
359
360     config_get_s(CONFIG_PLAYER, demo.player, MAXNAM);
361
362     strncpy(demo.shot, level->shot, PATHMAX);
363     strncpy(demo.file, level->file, PATHMAX);
364
365     demo.time   = t;
366     demo.goal   = g;
367     demo.goal_e = e;
368     demo.score  = s;
369     demo.balls  = b;
370     demo.times  = tt;
371
372     if ((demo_fp = fopen(demo.filename, FMODE_WB)))
373     {
374         demo_header_write(demo_fp, &demo);
375         audio_music_fade_to(2.0f, level->song);
376
377         /*
378          * Init both client and server, then process the first batch
379          * of commands generated by the server to sync client to
380          * server.
381          */
382
383         if (game_client_init(level->file) &&
384             game_server_init(level->file, t, e))
385         {
386             game_client_step(demo_fp);
387             return 1;
388         }
389     }
390     return 0;
391 }
392
393 void demo_play_stat(int status, int coins, int timer)
394 {
395     if (demo_fp)
396     {
397         long pos = ftell(demo_fp);
398
399         fseek(demo_fp, 8, SEEK_SET);
400
401         put_index(demo_fp, &timer);
402         put_index(demo_fp, &coins);
403         put_index(demo_fp, &status);
404
405         fseek(demo_fp, pos, SEEK_SET);
406     }
407 }
408
409 void demo_play_stop(void)
410 {
411     if (demo_fp)
412     {
413         fclose(demo_fp);
414         demo_fp = NULL;
415     }
416 }
417
418 int demo_saved(void)
419 {
420     return demo_exists(USER_REPLAY_FILE);
421 }
422
423 void demo_rename(const char *name)
424 {
425     char src[MAXSTR];
426     char dst[MAXSTR];
427
428     if (name &&
429         demo_exists(USER_REPLAY_FILE) &&
430         strcmp(name, USER_REPLAY_FILE) != 0)
431     {
432         strcpy(src, config_user(USER_REPLAY_FILE));
433         strcat(src, REPLAY_EXT);
434
435         strcpy(dst, config_user(name));
436         strcat(dst, REPLAY_EXT);
437
438         file_rename(src, dst);
439     }
440 }
441
442 void demo_rename_player(const char *name, const char *player)
443 {
444     char filename[MAXSTR];
445     FILE *old_fp, *new_fp;
446     struct demo d;
447
448     assert(name);
449     assert(player);
450
451     /* TODO: make this reusable. */
452
453     filename[sizeof (filename) - 1] = '\0';
454     strncpy(filename, name, sizeof (filename) - 1);
455     strncat(filename, REPLAY_EXT, sizeof (filename) - 1 - strlen(name));
456
457     /*
458      * Write out a temporary file containing the original replay data with a
459      * new player name, then copy the resulting contents back to the original
460      * file.
461      *
462      * (It is believed that the ugliness found here is outweighed by the
463      * benefits of preserving the arbitrary-length property of all strings in
464      * the replay.  In case of doubt, FIXME.)
465      */
466
467     if ((old_fp = fopen(config_user(filename), FMODE_RB)))
468     {
469         if ((new_fp = tmpfile()))
470         {
471             if (demo_header_read(old_fp, &d))
472             {
473                 FILE *save_fp;
474
475                 /* Modify and write the header. */
476
477                 strncpy(d.player, player, sizeof (d.player));
478
479                 demo_header_write(new_fp, &d);
480
481                 /* 
482                  * Restore the last three fields not written by the above call.
483                  */
484
485                 /* Hack, hack, hack. */
486
487                 save_fp = demo_fp;
488                 demo_fp = new_fp;
489
490                 demo_play_stat(d.status, d.coins, d.timer);
491
492                 demo_fp = save_fp;
493
494                 /* Copy the remaining data. */
495
496                 file_copy(old_fp, new_fp);
497
498                 /* Then copy everything back. */
499
500                 if (freopen(config_user(filename), FMODE_WB, old_fp))
501                 {
502                     fseek(new_fp, 0L, SEEK_SET);
503                     file_copy(new_fp, old_fp);
504                 }
505             }
506             fclose(new_fp);
507         }
508         fclose(old_fp);
509     }
510 }
511
512 /*---------------------------------------------------------------------------*/
513
514 static struct demo  demo_replay;       /* The current demo */
515 static struct level demo_level_replay; /* The current level demo-ed*/
516
517 const struct demo *curr_demo_replay(void)
518 {
519     return &demo_replay;
520 }
521
522 int demo_replay_init(const char *name, int *g, int *m, int *b, int *s, int *tt)
523 {
524     demo_fp = fopen(name, FMODE_RB);
525
526     if (demo_fp && demo_header_read(demo_fp, &demo_replay))
527     {
528         strncpy(demo_replay.filename, name, MAXSTR);
529         strncpy(demo_replay.name,
530                 base_name(text_from_locale(demo_replay.filename), REPLAY_EXT),
531                 PATHMAX);
532
533         if (level_load(demo_replay.file, &demo_level_replay))
534         {
535             demo_level_replay.time = demo_replay.time;
536             demo_level_replay.goal = demo_replay.goal;
537         }
538         else
539             return 0;
540
541         if (g)  *g  = demo_replay.goal;
542         if (m)  *m  = demo_replay.mode;
543         if (b)  *b  = demo_replay.balls;
544         if (s)  *s  = demo_replay.score;
545         if (tt) *tt = demo_replay.times;
546
547         /*
548          * Init client and then read and process the first batch of
549          * commands from the replay file.
550          */
551
552         if (g)
553         {
554             audio_music_fade_to(0.5f, demo_level_replay.song);
555
556             return (game_client_init(demo_level_replay.file) &&
557                     demo_replay_step(0.0f));
558         }
559
560         /* Likewise, but also queue a command to open the goal. */
561
562         else if (game_client_init(demo_level_replay.file))
563         {
564             union cmd cmd;
565
566             cmd.type = CMD_GOAL_OPEN;
567             game_proxy_enq(&cmd);
568
569             return demo_replay_step(0.0f);
570         }
571     }
572     return 0;
573 }
574
575 /*
576  * Read and enqueue commands from the replay file, up to and including
577  * CMD_END_OF_UPDATE.  Return 0 on EOF.  Otherwise, run the commands
578  * on the client and return 1.
579  */
580 int demo_replay_step(float dt)
581 {
582     if (demo_fp)
583     {
584         union cmd cmd;
585
586         while (cmd_get(demo_fp, &cmd))
587         {
588             game_proxy_enq(&cmd);
589
590             if (cmd.type == CMD_END_OF_UPDATE)
591                 break;
592         }
593
594         if (!feof(demo_fp))
595         {
596             game_client_step(NULL);
597             return 1;
598         }
599     }
600     return 0;
601 }
602
603 void demo_replay_stop(int d)
604 {
605     if (demo_fp)
606     {
607         fclose(demo_fp);
608         demo_fp = NULL;
609
610         if (d) remove(demo_replay.filename);
611     }
612 }
613
614 void demo_replay_dump_info(void)
615 {
616     demo_dump_info(&demo_replay);
617 }
618
619 /*---------------------------------------------------------------------------*/
620
621 FILE *demo_file(void) { return demo_fp; }
622
623 /*---------------------------------------------------------------------------*/