- Fixed a bug that made Nitfol crash since [130]
[rodin/chimara.git] / libchimara / chimara-if.c
1 #include <errno.h>
2 #include <stdlib.h>
3 #include <glib.h>
4 #include <glib-object.h>
5 #include <config.h>
6 #include <glib/gi18n-lib.h>
7 #include "chimara-if.h"
8 #include "chimara-glk.h"
9 #include "chimara-marshallers.h"
10 #include "init.h"
11
12 static gboolean supported_formats[CHIMARA_IF_NUM_FORMATS][CHIMARA_IF_NUM_INTERPRETERS] = {
13         /* Frotz Nitfol Glulxe Git */
14         { TRUE,  TRUE,  FALSE, FALSE }, /* Z5 */
15         { TRUE,  TRUE,  FALSE, FALSE }, /* Z6 */
16         { TRUE,  TRUE,  FALSE, FALSE }, /* Z8 */
17         { TRUE,  TRUE,  FALSE, FALSE }, /* Zblorb */
18         { FALSE, FALSE, TRUE,  TRUE  }, /* Glulx */
19         { FALSE, FALSE, TRUE,  TRUE  }  /* Gblorb */
20 };
21 static gchar *format_names[CHIMARA_IF_NUM_FORMATS] = {
22         N_("Z-code version 5"),
23         N_("Z-code version 6"),
24         N_("Z-code version 8"),
25         N_("Blorbed Z-code"),
26         N_("Glulx"),
27         N_("Blorbed Glulx")
28 };
29 static gchar *interpreter_names[CHIMARA_IF_NUM_INTERPRETERS] = {
30         N_("Frotz"), N_("Nitfol"), N_("Glulxe"), N_("Git")
31 };
32 static gchar *plugin_names[CHIMARA_IF_NUM_INTERPRETERS] = {
33         "frotz", "nitfol", "glulxe", "git"
34 };
35
36 typedef enum _ChimaraIFFlags {
37         CHIMARA_IF_PIRACY_MODE = 1 << 0,
38         CHIMARA_IF_TANDY_BIT = 1 << 1,
39         CHIMARA_IF_EXPAND_ABBREVIATIONS = 1 << 2,
40         CHIMARA_IF_IGNORE_ERRORS = 1 << 3,
41         CHIMARA_IF_TYPO_CORRECTION = 1 << 4
42 } ChimaraIFFlags;
43
44 typedef struct _ChimaraIFPrivate {
45         ChimaraIFInterpreter preferred_interpreter[CHIMARA_IF_NUM_FORMATS];
46         ChimaraIFFormat format;
47         ChimaraIFInterpreter interpreter;
48         ChimaraIFFlags flags;
49         ChimaraIFZmachineVersion interpreter_number;
50         gint random_seed;
51         gboolean random_seed_set;
52         /* Holding buffers for input and response */
53         gchar *input;
54         GString *response;
55 } ChimaraIFPrivate;
56
57 #define CHIMARA_IF_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE((o), CHIMARA_TYPE_IF, ChimaraIFPrivate))
58 #define CHIMARA_IF_USE_PRIVATE(o, n) ChimaraIFPrivate *n = CHIMARA_IF_PRIVATE(o)
59
60 enum {
61         PROP_0,
62         PROP_PIRACY_MODE,
63         PROP_TANDY_BIT,
64         PROP_EXPAND_ABBREVIATIONS,
65         PROP_IGNORE_ERRORS,
66         PROP_TYPO_CORRECTION,
67         PROP_INTERPRETER_NUMBER,
68         PROP_RANDOM_SEED,
69         PROP_RANDOM_SEED_SET
70 };
71
72 enum {
73         COMMAND,
74         LAST_SIGNAL
75 };
76
77 static guint chimara_if_signals[LAST_SIGNAL] = { 0 };
78
79 G_DEFINE_TYPE(ChimaraIF, chimara_if, CHIMARA_TYPE_GLK);
80
81 static void
82 chimara_if_waiting(ChimaraGlk *glk)
83 {
84         CHIMARA_IF_USE_PRIVATE(glk, priv);
85
86         gchar *response = g_string_free(priv->response, FALSE);
87         priv->response = g_string_new("");
88         
89         g_signal_emit_by_name(glk, "command", priv->input, response);
90         
91         g_free(priv->input);
92         g_free(response);
93         priv->input = NULL;
94 }
95
96 static void
97 chimara_if_stopped(ChimaraGlk *glk)
98 {
99         CHIMARA_IF_USE_PRIVATE(glk, priv);
100         
101         if(priv->input || priv->response->len > 0)
102                 chimara_if_waiting(glk); /* Send one last command signal */
103         
104         priv->format = CHIMARA_IF_FORMAT_NONE;
105         priv->interpreter = CHIMARA_IF_INTERPRETER_NONE;
106 }
107
108 static void
109 chimara_if_line_input(ChimaraGlk *glk, guint32 win_rock, gchar *input)
110 {
111         CHIMARA_IF_USE_PRIVATE(glk, priv);
112         g_assert(priv->input == NULL);
113         priv->input = g_strdup(input);
114 }
115
116 static void
117 chimara_if_text_buffer_output(ChimaraGlk *glk, guint32 win_rock, gchar *output)
118 {
119         CHIMARA_IF_USE_PRIVATE(glk, priv);
120         g_string_append(priv->response, output);
121 }
122
123 static void
124 chimara_if_init(ChimaraIF *self)
125 {
126         CHIMARA_IF_USE_PRIVATE(self, priv);
127         priv->preferred_interpreter[CHIMARA_IF_FORMAT_Z5] = CHIMARA_IF_INTERPRETER_FROTZ;
128         priv->preferred_interpreter[CHIMARA_IF_FORMAT_Z6] = CHIMARA_IF_INTERPRETER_FROTZ;
129         priv->preferred_interpreter[CHIMARA_IF_FORMAT_Z8] = CHIMARA_IF_INTERPRETER_FROTZ;
130         priv->preferred_interpreter[CHIMARA_IF_FORMAT_Z_BLORB] = CHIMARA_IF_INTERPRETER_FROTZ;
131         priv->preferred_interpreter[CHIMARA_IF_FORMAT_GLULX] = CHIMARA_IF_INTERPRETER_GLULXE;
132         priv->preferred_interpreter[CHIMARA_IF_FORMAT_GLULX_BLORB] = CHIMARA_IF_INTERPRETER_GLULXE;
133         priv->format = CHIMARA_IF_FORMAT_NONE;
134         priv->interpreter = CHIMARA_IF_INTERPRETER_NONE;
135         priv->flags = CHIMARA_IF_TYPO_CORRECTION;
136         priv->interpreter_number = CHIMARA_IF_ZMACHINE_DEFAULT;
137         priv->random_seed_set = FALSE;
138         priv->input = NULL;
139         priv->response = g_string_new("");
140         
141         /* Connect to signals of ChimaraGlk parent */
142         g_signal_connect(self, "stopped", G_CALLBACK(chimara_if_stopped), NULL);
143         g_signal_connect(self, "waiting", G_CALLBACK(chimara_if_waiting), NULL);
144         g_signal_connect(self, "line-input", G_CALLBACK(chimara_if_line_input), NULL);
145         g_signal_connect(self, "text-buffer-output", G_CALLBACK(chimara_if_text_buffer_output), NULL);
146 }
147
148 #define PROCESS_FLAG(flags, flag, val) (flags) = (val)? (flags) | (flag) : (flags) & ~(flag)
149
150 static void
151 chimara_if_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
152 {
153         CHIMARA_IF_USE_PRIVATE(object, priv);
154     switch(prop_id)
155     {
156         case PROP_PIRACY_MODE:
157                 PROCESS_FLAG(priv->flags, CHIMARA_IF_PIRACY_MODE, g_value_get_boolean(value));
158                 break;
159         case PROP_TANDY_BIT:
160                 PROCESS_FLAG(priv->flags, CHIMARA_IF_TANDY_BIT, g_value_get_boolean(value));
161                 break;
162         case PROP_EXPAND_ABBREVIATIONS:
163                 PROCESS_FLAG(priv->flags, CHIMARA_IF_EXPAND_ABBREVIATIONS, g_value_get_boolean(value));
164                 break;
165         case PROP_IGNORE_ERRORS:
166                 PROCESS_FLAG(priv->flags, CHIMARA_IF_IGNORE_ERRORS, g_value_get_boolean(value));
167                 break;
168         case PROP_TYPO_CORRECTION:
169                 PROCESS_FLAG(priv->flags, CHIMARA_IF_TYPO_CORRECTION, g_value_get_boolean(value));
170                 break;
171         case PROP_INTERPRETER_NUMBER:
172                 priv->interpreter_number = g_value_get_uint(value);
173                 break;
174         case PROP_RANDOM_SEED:
175                 priv->random_seed = g_value_get_int(value);
176                 break;
177         case PROP_RANDOM_SEED_SET:
178                 priv->random_seed_set = g_value_get_boolean(value);
179                 break;
180         default:
181             G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
182     }
183 }
184
185 static void
186 chimara_if_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
187 {
188         CHIMARA_IF_USE_PRIVATE(object, priv);
189     switch(prop_id)
190     {
191         case PROP_PIRACY_MODE:
192                 g_value_set_boolean(value, priv->flags & CHIMARA_IF_PIRACY_MODE);
193                 break;
194         case PROP_TANDY_BIT:
195                 g_value_set_boolean(value, priv->flags & CHIMARA_IF_TANDY_BIT);
196                 break;
197         case PROP_EXPAND_ABBREVIATIONS:
198                 g_value_set_boolean(value, priv->flags & CHIMARA_IF_EXPAND_ABBREVIATIONS);
199                 break;
200         case PROP_IGNORE_ERRORS:
201                 g_value_set_boolean(value, priv->flags & CHIMARA_IF_IGNORE_ERRORS);
202                 break;
203         case PROP_TYPO_CORRECTION:
204                 g_value_set_boolean(value, priv->flags & CHIMARA_IF_TYPO_CORRECTION);
205                 break;
206         case PROP_INTERPRETER_NUMBER:
207                 g_value_set_uint(value, priv->interpreter_number);
208                 break;
209         case PROP_RANDOM_SEED:
210                 g_value_set_int(value, priv->random_seed);
211                 break;
212         case PROP_RANDOM_SEED_SET:
213                 g_value_set_boolean(value, priv->random_seed_set);
214                 break;
215         default:
216             G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
217     }
218 }
219
220 static void
221 chimara_if_finalize(GObject *object)
222 {
223     G_OBJECT_CLASS(chimara_if_parent_class)->finalize(object);
224 }
225
226 static void
227 chimara_if_command(ChimaraIF *self, gchar *input, gchar *response)
228 {
229         /* Default signal handler */
230 }
231
232 /* G_PARAM_STATIC_STRINGS only appeared in GTK 2.13.0 */
233 #ifndef G_PARAM_STATIC_STRINGS
234 #define G_PARAM_STATIC_STRINGS (G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB)
235 #endif
236
237 static void
238 chimara_if_class_init(ChimaraIFClass *klass)
239 {
240         /* Override methods of parent classes */
241         GObjectClass *object_class = G_OBJECT_CLASS(klass);
242         object_class->set_property = chimara_if_set_property;
243         object_class->get_property = chimara_if_get_property;
244         object_class->finalize = chimara_if_finalize;
245
246         /* Signals */
247         klass->command = chimara_if_command;
248         /* Gtk-Doc for command */
249         chimara_if_signals[COMMAND] = g_signal_new("command",
250                 G_OBJECT_CLASS_TYPE(klass), 0,
251                 G_STRUCT_OFFSET(ChimaraIFClass, command), NULL, NULL,
252                 chimara_marshal_VOID__STRING_STRING,
253                 G_TYPE_NONE, 2, G_TYPE_STRING, G_TYPE_STRING);
254
255         /* Properties */
256         g_object_class_install_property(object_class, PROP_PIRACY_MODE,
257                 g_param_spec_boolean("piracy-mode", _("Piracy mode"), 
258                 _("Pretend the game is pirated"), FALSE,
259                 G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_LAX_VALIDATION | G_PARAM_STATIC_STRINGS));
260         /**
261          * ChimaraIF:tandy-bit:
262          * 
263          * Some early Infocom games were sold by the Tandy Corporation. Setting this
264          * property to %TRUE changes the wording of some Version 3 Infocom games 
265          * slightly, so as to be less offensive. See <ulink 
266          * url="http://www.ifarchive.org/if-archive/infocom/info/tandy_bits.html">
267          * http://www.ifarchive.org/if-archive/infocom/info/tandy_bits.html</ulink>.
268          * 
269          * Only works on Z-machine interpreters.
270          */
271         g_object_class_install_property(object_class, PROP_TANDY_BIT,
272                 g_param_spec_boolean("tandy-bit", _("Tandy bit"), 
273                 _("Censor certain Infocom games"), FALSE, 
274                 G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_LAX_VALIDATION | G_PARAM_STATIC_STRINGS));
275         
276         g_object_class_install_property(object_class, PROP_EXPAND_ABBREVIATIONS,
277                 g_param_spec_boolean("expand-abbreviations", _("Expand abbreviations"),
278                 _("Expand abbreviations such as X for EXAMINE"), FALSE,
279                 G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_LAX_VALIDATION | G_PARAM_STATIC_STRINGS));
280         
281         g_object_class_install_property(object_class, PROP_IGNORE_ERRORS,
282                 g_param_spec_boolean("ignore-errors", _("Ignore errors"), 
283                 _("Do not warn the user about Z-machine errors"), FALSE,
284                 G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_LAX_VALIDATION | G_PARAM_STATIC_STRINGS));
285         
286         g_object_class_install_property(object_class, PROP_TYPO_CORRECTION,
287                 g_param_spec_boolean("typo-correction", _("Typo correction"),
288                 _("Try to remedy typos if the interpreter supports it"), TRUE,
289                 G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_LAX_VALIDATION | G_PARAM_STATIC_STRINGS));
290         
291         g_object_class_install_property(object_class, PROP_INTERPRETER_NUMBER,
292                 g_param_spec_uint("interpreter-number", _("Interpreter number"),
293                 _("Platform the Z-machine should pretend it is running on"), 
294                 CHIMARA_IF_ZMACHINE_DEFAULT, CHIMARA_IF_ZMACHINE_MAXVAL, CHIMARA_IF_ZMACHINE_DEFAULT,
295                 G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_LAX_VALIDATION | G_PARAM_STATIC_STRINGS));
296         
297         g_object_class_install_property(object_class, PROP_RANDOM_SEED,
298                 g_param_spec_int("random-seed", _("Random seed"),
299                 _("Seed for the random number generator"), G_MININT, G_MAXINT, 0,
300                 G_PARAM_READWRITE | G_PARAM_LAX_VALIDATION | G_PARAM_STATIC_STRINGS));
301                 
302         g_object_class_install_property(object_class, PROP_RANDOM_SEED_SET,
303                 g_param_spec_boolean("random-seed-set", _("Random seed set"),
304                 _("Whether the seed for the random number generator should be set manually"), FALSE,
305                 G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_LAX_VALIDATION | G_PARAM_STATIC_STRINGS));
306
307         /* Private data */
308         g_type_class_add_private(klass, sizeof(ChimaraIFPrivate));
309 }
310
311 /* PUBLIC FUNCTIONS */
312
313 /**
314  * chimara_if_new:
315  *
316  * Creates and initializes a new #ChimaraIF widget.
317  *
318  * Return value: a #ChimaraIF widget, with a floating reference.
319  */
320 GtkWidget *
321 chimara_if_new(void)
322 {
323         /* This is a library entry point; initialize the library */
324         chimara_init();
325     return GTK_WIDGET(g_object_new(CHIMARA_TYPE_IF, NULL));
326 }
327
328 void
329 chimara_if_set_preferred_interpreter(ChimaraIF *self, ChimaraIFFormat format, ChimaraIFInterpreter interpreter)
330 {
331         g_return_if_fail(self && CHIMARA_IS_IF(self));
332         g_return_if_fail(format < CHIMARA_IF_NUM_FORMATS);
333         g_return_if_fail(format < CHIMARA_IF_NUM_INTERPRETERS);
334
335         CHIMARA_IF_USE_PRIVATE(self, priv);
336
337         if(supported_formats[format][interpreter])
338                 priv->preferred_interpreter[format] = interpreter;
339         else
340                 g_warning("Format '%s' is not supported by interpreter '%s'", format_names[format], interpreter_names[interpreter]);
341 }
342
343 ChimaraIFInterpreter
344 chimara_if_get_preferred_interpreter(ChimaraIF *self, ChimaraIFFormat format)
345 {
346         g_return_val_if_fail(self && CHIMARA_IS_IF(self), -1);
347         g_return_val_if_fail(format < CHIMARA_IF_NUM_FORMATS, -1);
348         CHIMARA_IF_USE_PRIVATE(self, priv);
349         return priv->preferred_interpreter[format];
350 }
351
352 gboolean 
353 chimara_if_run_game(ChimaraIF *self, gchar *gamefile, GError **error)
354 {
355         g_return_val_if_fail(self && CHIMARA_IS_IF(self), FALSE);
356         g_return_val_if_fail(gamefile, FALSE);
357         
358         CHIMARA_IF_USE_PRIVATE(self, priv);
359
360         /* Find out what format the game is */
361         /* TODO: Look inside the file instead of just looking at the extension */
362         ChimaraIFFormat format = CHIMARA_IF_FORMAT_Z5;
363         if(g_str_has_suffix(gamefile, ".z5"))
364                 format = CHIMARA_IF_FORMAT_Z5;
365         else if(g_str_has_suffix(gamefile, ".z6"))
366                 format = CHIMARA_IF_FORMAT_Z6;
367         else if(g_str_has_suffix(gamefile, ".z8"))
368                 format = CHIMARA_IF_FORMAT_Z8;
369         else if(g_str_has_suffix(gamefile, ".zlb") || g_str_has_suffix(gamefile, ".zblorb"))
370                 format = CHIMARA_IF_FORMAT_Z_BLORB;
371         else if(g_str_has_suffix(gamefile, ".ulx"))
372                 format = CHIMARA_IF_FORMAT_GLULX;
373         else if(g_str_has_suffix(gamefile, ".blb") || g_str_has_suffix(gamefile, ".blorb") || g_str_has_suffix(gamefile, ".glb") || g_str_has_suffix(gamefile, ".gblorb"))
374                 format = CHIMARA_IF_FORMAT_GLULX_BLORB;
375         
376         /* Now decide what interpreter to use */
377         ChimaraIFInterpreter interpreter = priv->preferred_interpreter[format];
378         gchar *pluginfile = g_strconcat(plugin_names[interpreter], "." G_MODULE_SUFFIX, NULL);
379
380         /* If there is a plugin in the source tree, use that */
381         gchar *pluginpath = g_build_filename("..", "interpreters", plugin_names[interpreter], LT_OBJDIR, pluginfile, NULL);
382         if( !g_file_test(pluginpath, G_FILE_TEST_EXISTS) ) 
383         {
384                 g_free(pluginpath);
385                 pluginpath = g_build_filename(PLUGINDIR, pluginfile, NULL);
386                 if( !g_file_test(pluginpath, G_FILE_TEST_EXISTS) ) 
387                 {
388                         g_free(pluginpath);
389                         g_free(pluginfile);
390                         g_set_error(error, CHIMARA_ERROR, CHIMARA_PLUGIN_NOT_FOUND, _("No appropriate %s interpreter plugin was found"), interpreter_names[interpreter]);
391                         return FALSE;
392                 }
393         }
394         g_free(pluginfile);
395
396         /* Decide what arguments to pass to the interpreters; currently only the
397         Z-machine interpreters accept command line arguments other than the game */
398         GSList *args = NULL;
399         gchar *terpnumstr = NULL, *randomstr = NULL;
400         args = g_slist_prepend(args, pluginpath);
401         args = g_slist_prepend(args, gamefile);
402         switch(interpreter)
403         {
404                 case CHIMARA_IF_INTERPRETER_FROTZ:
405                         if(priv->flags & CHIMARA_IF_PIRACY_MODE)
406                                 args = g_slist_prepend(args, "-P");
407                         if(priv->flags & CHIMARA_IF_TANDY_BIT)
408                                 args = g_slist_prepend(args, "-t");
409                         if(priv->flags & CHIMARA_IF_EXPAND_ABBREVIATIONS)
410                                 args = g_slist_prepend(args, "-x");
411                         if(priv->flags & CHIMARA_IF_IGNORE_ERRORS)
412                                 args = g_slist_prepend(args, "-i");
413                         if(priv->interpreter_number != CHIMARA_IF_ZMACHINE_DEFAULT)
414                         {
415                                 terpnumstr = g_strdup_printf("-I%u", priv->interpreter_number);
416                                 args = g_slist_prepend(args, terpnumstr);
417                         }
418                         if(priv->random_seed_set)
419                         {
420                                 randomstr = g_strdup_printf("-s%d", priv->random_seed);
421                                 args = g_slist_prepend(args, randomstr);
422                         }
423                         break;
424                 case CHIMARA_IF_INTERPRETER_NITFOL:
425                         if(priv->flags & CHIMARA_IF_PIRACY_MODE)
426                                 args = g_slist_prepend(args, "-pirate");
427                         if(priv->flags & CHIMARA_IF_TANDY_BIT)
428                                 args = g_slist_prepend(args, "-tandy");
429                         if(!(priv->flags & CHIMARA_IF_EXPAND_ABBREVIATIONS))
430                                 args = g_slist_prepend(args, "-no-expand");
431                         if(priv->flags & CHIMARA_IF_IGNORE_ERRORS)
432                                 args = g_slist_prepend(args, "-ignore");
433                         if(!(priv->flags & CHIMARA_IF_TYPO_CORRECTION))
434                                 args = g_slist_prepend(args, "-no-spell");
435                         if(priv->interpreter_number != CHIMARA_IF_ZMACHINE_DEFAULT)
436                         {
437                                 terpnumstr = g_strdup_printf("-terpnum%u", priv->interpreter_number);
438                                 args = g_slist_prepend(args, terpnumstr);
439                         }
440                         if(priv->random_seed_set)
441                         {
442                                 randomstr = g_strdup_printf("-random%d", priv->random_seed);
443                                 args = g_slist_prepend(args, randomstr);
444                         }
445                         break;
446                 default:
447                         ;
448         }
449         
450         /* Allocate argv to hold the arguments */
451         int argc = g_slist_length(args);
452         args = g_slist_prepend(args, NULL);
453         char **argv = g_new0(char *, argc + 1);
454         
455         /* Fill argv */
456         args = g_slist_reverse(args);
457         int count;
458         GSList *ptr;
459         for(count = 0, ptr = args; ptr; count++, ptr = g_slist_next(ptr))
460                 argv[count] = ptr->data;
461         
462         gboolean retval = chimara_glk_run(CHIMARA_GLK(self), pluginpath, argc, argv, error);
463         g_free(argv);
464         if(terpnumstr)
465                 g_free(terpnumstr);
466         if(randomstr)
467                 g_free(randomstr);
468         g_free(pluginpath);
469         
470         /* Set current format and interpreter if plugin was started successfully */
471         if(retval)
472         {
473                 priv->format = format;
474                 priv->interpreter = interpreter;
475         }
476         return retval;
477 }