Core commit. Compile and run on the OpenPandora
[mupen64plus-pandora.git] / source / mupen64plus-core / src / api / config.c
1 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
2  *   Mupen64plus-core - api/config.c                                       *
3  *   Mupen64Plus homepage: http://code.google.com/p/mupen64plus/           *
4  *   Copyright (C) 2009 Richard Goedeken                                   *
5  *                                                                         *
6  *   This program is free software; you can redistribute it and/or modify  *
7  *   it under the terms of the GNU General Public License as published by  *
8  *   the Free Software Foundation; either version 2 of the License, or     *
9  *   (at your option) any later version.                                   *
10  *                                                                         *
11  *   This program is distributed in the hope that it will be useful,       * 
12  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
13  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
14  *   GNU General Public License for more details.                          *
15  *                                                                         *
16  *   You should have received a copy of the GNU General Public License     *
17  *   along with this program; if not, write to the                         *
18  *   Free Software Foundation, Inc.,                                       *
19  *   51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.          *
20  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
21                        
22 /* This file contains the Core config functions which will be exported
23  * outside of the core library.
24  */
25
26 #include <stdio.h>
27 #include <stdlib.h>
28 #include <string.h>
29
30 #define M64P_CORE_PROTOTYPES 1
31 #include "m64p_types.h"
32 #include "m64p_config.h"
33 #include "config.h"
34 #include "callbacks.h"
35
36 #include "main/util.h"
37
38 #include "osal/files.h"
39 #include "osal/preproc.h"
40
41 /* local types */
42 #define MUPEN64PLUS_CFG_NAME "mupen64plus.cfg"
43
44 #define SECTION_MAGIC 0xDBDC0580
45
46 typedef struct _config_var {
47   char                 *name;
48   m64p_type             type;
49   union {
50     int integer;
51     float number;
52     char *string;
53   } val;
54   char                 *comment;
55   struct _config_var   *next;
56   } config_var;
57
58 typedef struct _config_section {
59   int                     magic;
60   char                   *name;
61   struct _config_var     *first_var;
62   struct _config_section *next;
63   } config_section;
64
65 typedef config_section *config_list;
66
67 /* local variables */
68 static int         l_ConfigInit = 0;
69 static int         l_SaveConfigOnExit = 0;
70 static char       *l_DataDirOverride = NULL;
71 static char       *l_ConfigDirOverride = NULL;
72 static config_list l_ConfigListActive = NULL;
73 static config_list l_ConfigListSaved = NULL;
74
75 /* --------------- */
76 /* local functions */
77 /* --------------- */
78
79 static int is_numeric(const char *string)
80 {
81     char chTemp[16];
82     float fTemp;
83     int rval = sscanf(string, "%f%8s", &fTemp, chTemp);
84
85     /* I want to find exactly one matched input item: a number with no garbage on the end */
86     /* I use sscanf() instead of a custom loop because this routine must handle locales in which the decimal separator is not '.' */
87     return (rval == 1);
88 }
89
90 /* This function returns a pointer to the pointer of the requested section
91  * (i.e. a pointer the next field of the previous element, or to the first node).
92  *
93  * If there's no section named 'ParamName', returns the pointer to the next
94  * field of the last element in the list (such that derefencing it is NULL).
95  *
96  * Useful for operations that need to modify the links, e.g. deleting a section.
97  */
98 static config_section **find_section_link(config_list *list, const char *ParamName)
99 {
100     config_section **curr_sec_link;
101     for (curr_sec_link = list; *curr_sec_link != NULL; curr_sec_link = &(*curr_sec_link)->next)
102     {
103         if (osal_insensitive_strcmp(ParamName, (*curr_sec_link)->name) == 0)
104             break;
105     }
106
107     return curr_sec_link;
108 }
109
110 /* This function is similar to the previous function, but instead it returns a
111  * pointer to the pointer to the next section whose name is alphabetically
112  * greater than or equal to 'ParamName'.
113  *
114  * Useful for inserting a section in its alphabetical position.
115  */
116 static config_section **find_alpha_section_link(config_list *list, const char *ParamName)
117 {
118     config_section **curr_sec_link;
119     for (curr_sec_link = list; *curr_sec_link != NULL; curr_sec_link = &(*curr_sec_link)->next)
120     {
121         if (osal_insensitive_strcmp((*curr_sec_link)->name, ParamName) >= 0)
122             break;
123     }
124
125     return curr_sec_link;
126 }
127
128 static config_section *find_section(config_list list, const char *ParamName)
129 {
130     return *find_section_link(&list, ParamName);
131 }
132
133 static config_var *config_var_create(const char *ParamName, const char *ParamHelp)
134 {
135     config_var *var = (config_var *) malloc(sizeof(config_var));
136
137     if (var == NULL || ParamName == NULL)
138         return NULL;
139
140     memset(var, 0, sizeof(config_var));
141
142     var->name = strdup(ParamName);
143     if (var->name == NULL)
144     {
145         free(var);
146         return NULL;
147     }
148
149     var->type = M64TYPE_INT;
150     var->val.integer = 0;
151
152     if (ParamHelp != NULL)
153     {
154         var->comment = strdup(ParamHelp);
155         if (var->comment == NULL)
156         {
157             free(var->name);
158             free(var);
159             return NULL;
160         }
161     }
162     else
163         var->comment = NULL;
164
165     var->next = NULL;
166     return var;
167 }
168
169 static config_var *find_section_var(config_section *section, const char *ParamName)
170 {
171     /* walk through the linked list of variables in the section */
172     config_var *curr_var;
173     for (curr_var = section->first_var; curr_var != NULL; curr_var = curr_var->next)
174     {
175         if (osal_insensitive_strcmp(ParamName, curr_var->name) == 0)
176             return curr_var;
177     }
178
179     /* couldn't find this configuration parameter */
180     return NULL;
181 }
182
183 static void append_var_to_section(config_section *section, config_var *var)
184 {
185     config_var *last_var;
186
187     if (section == NULL || var == NULL || section->magic != SECTION_MAGIC)
188         return;
189
190     if (section->first_var == NULL)
191     {
192         section->first_var = var;
193         return;
194     }
195
196     last_var = section->first_var;
197     while (last_var->next != NULL)
198         last_var = last_var->next;
199
200     last_var->next = var;
201 }
202
203 static void delete_var(config_var *var)
204 {
205     if (var->type == M64TYPE_STRING)
206         free(var->val.string);
207     free(var->name);
208     free(var->comment);
209     free(var);
210 }
211
212 static void delete_section(config_section *pSection)
213 {
214     config_var *curr_var;
215
216     if (pSection == NULL)
217         return;
218
219     curr_var = pSection->first_var;
220     while (curr_var != NULL)
221     {
222         config_var *next_var = curr_var->next;
223         delete_var(curr_var);
224         curr_var = next_var;
225     }
226
227     free(pSection->name);
228     free(pSection);
229 }
230
231 static void delete_list(config_list *pConfigList)
232 {
233     config_section *curr_section = *pConfigList;
234     while (curr_section != NULL)
235     {
236         config_section *next_section = curr_section->next;
237         /* delete the section itself */
238         delete_section(curr_section);
239
240         curr_section = next_section;
241     }
242
243     *pConfigList = NULL;
244 }
245
246 static config_section *config_section_create(const char *ParamName)
247 {
248     config_section *sec;
249
250     if (ParamName == NULL)
251         return NULL;
252
253     sec = (config_section *) malloc(sizeof(config_section));
254     if (sec == NULL)
255         return NULL;
256
257     sec->magic = SECTION_MAGIC;
258     sec->name = strdup(ParamName);
259     if (sec->name == NULL)
260     {
261         free(sec);
262         return NULL;
263     }
264     sec->first_var = NULL;
265     sec->next = NULL;
266     return sec;
267 }
268
269 static config_section * section_deepcopy(config_section *orig_section)
270 {
271     config_section *new_section;
272     config_var *orig_var, *last_new_var;
273
274     /* Input validation */
275     if (orig_section == NULL)
276         return NULL;
277
278     /* create and copy section struct */
279     new_section = config_section_create(orig_section->name);
280     if (new_section == NULL)
281         return NULL;
282
283     /* create and copy all section variables */
284     orig_var = orig_section->first_var;
285     last_new_var = NULL;
286     while (orig_var != NULL)
287     {
288         config_var *new_var = config_var_create(orig_var->name, orig_var->comment);
289         if (new_var == NULL)
290         {
291             delete_section(new_section);
292             return NULL;
293         }
294
295         new_var->type = orig_var->type;
296         switch (orig_var->type)
297         {
298             case M64TYPE_INT:
299             case M64TYPE_BOOL:
300                 new_var->val.integer = orig_var->val.integer;
301                 break;
302                 
303             case M64TYPE_FLOAT:
304                 new_var->val.number = orig_var->val.number;
305                 break;
306
307             case M64TYPE_STRING:
308                 if (orig_var->val.string != NULL)
309                 {
310                     new_var->val.string = strdup(orig_var->val.string);
311                     if (new_var->val.string == NULL)
312                     {
313                         delete_section(new_section);
314                         return NULL;
315                     }
316                 }
317                 else
318                     new_var->val.string = NULL;
319
320                 break;
321         }
322
323         /* add the new variable to the new section */
324         if (last_new_var == NULL)
325             new_section->first_var = new_var;
326         else
327             last_new_var->next = new_var;
328         last_new_var = new_var;
329         /* advance variable pointer in original section variable list */
330         orig_var = orig_var->next;
331     }
332
333     return new_section;
334 }
335
336 static void copy_configlist_active_to_saved(void)
337 {
338     config_section *curr_section = l_ConfigListActive;
339     config_section *last_section = NULL;
340
341     /* delete any pre-existing Saved config list */
342     delete_list(&l_ConfigListSaved);
343
344     /* duplicate all of the config sections in the Active list, adding them to the Saved list */
345     while (curr_section != NULL)
346     {
347         config_section *new_section = section_deepcopy(curr_section);
348         if (new_section == NULL) break;
349         if (last_section == NULL)
350             l_ConfigListSaved = new_section;
351         else
352             last_section->next = new_section;
353         last_section = new_section;
354         curr_section = curr_section->next;
355     }
356 }
357
358 static m64p_error write_configlist_file(void)
359 {
360     config_section *curr_section;
361     const char *configpath;
362     char *filepath;
363     FILE *fPtr;
364
365     /* get the full pathname to the config file and try to open it */
366     configpath = ConfigGetUserConfigPath();
367     if (configpath == NULL)
368         return M64ERR_FILES;
369
370     filepath = combinepath(configpath, MUPEN64PLUS_CFG_NAME);
371     if (filepath == NULL)
372         return M64ERR_NO_MEMORY;
373
374     fPtr = fopen(filepath, "wb"); 
375     if (fPtr == NULL)
376     {
377         DebugMessage(M64MSG_ERROR, "Couldn't open configuration file '%s' for writing.", filepath);
378         free(filepath);
379         return M64ERR_FILES;
380     }
381     free(filepath);
382
383     /* write out header */
384     fprintf(fPtr, "# Mupen64Plus Configuration File\n");
385     fprintf(fPtr, "# This file is automatically read and written by the Mupen64Plus Core library\n");
386
387     /* write out all of the config parameters from the Saved list */
388     curr_section = l_ConfigListSaved;
389     while (curr_section != NULL)
390     {
391         config_var *curr_var = curr_section->first_var;
392         fprintf(fPtr, "\n[%s]\n\n", curr_section->name);
393         while (curr_var != NULL)
394         {
395             if (curr_var->comment != NULL && strlen(curr_var->comment) > 0)
396                 fprintf(fPtr, "# %s\n", curr_var->comment);
397             if (curr_var->type == M64TYPE_INT)
398                 fprintf(fPtr, "%s = %i\n", curr_var->name, curr_var->val.integer);
399             else if (curr_var->type == M64TYPE_FLOAT)
400                 fprintf(fPtr, "%s = %f\n", curr_var->name, curr_var->val.number);
401             else if (curr_var->type == M64TYPE_BOOL && curr_var->val.integer)
402                 fprintf(fPtr, "%s = True\n", curr_var->name);
403             else if (curr_var->type == M64TYPE_BOOL && !curr_var->val.integer)
404                 fprintf(fPtr, "%s = False\n", curr_var->name);
405             else if (curr_var->type == M64TYPE_STRING && curr_var->val.string != NULL)
406                 fprintf(fPtr, "%s = \"%s\"\n", curr_var->name, curr_var->val.string);
407             curr_var = curr_var->next;
408         }
409         fprintf(fPtr, "\n");
410         curr_section = curr_section->next;
411     }
412
413     fclose(fPtr);
414     return M64ERR_SUCCESS;
415 }
416
417 /* ----------------------------------------------------------- */
418 /* these functions are only to be used within the Core library */
419 /* ----------------------------------------------------------- */
420
421 m64p_error ConfigInit(const char *ConfigDirOverride, const char *DataDirOverride)
422 {
423     m64p_error rval;
424     const char *configpath = NULL;
425     char *filepath;
426     long filelen;
427     FILE *fPtr;
428     char *configtext;
429
430     config_section *current_section = NULL;
431     char *line, *end, *lastcomment;
432
433     if (l_ConfigInit)
434         return M64ERR_ALREADY_INIT;
435     l_ConfigInit = 1;
436
437     /* if a data directory was specified, make a copy of it */
438     if (DataDirOverride != NULL)
439     {
440         l_DataDirOverride = strdup(DataDirOverride);
441         if (l_DataDirOverride == NULL)
442             return M64ERR_NO_MEMORY;
443     }
444
445     /* if a config directory was specified, make a copy of it */
446     if (ConfigDirOverride != NULL)
447     {
448         l_ConfigDirOverride = strdup(ConfigDirOverride);
449         if (l_ConfigDirOverride == NULL)
450             return M64ERR_NO_MEMORY;
451     }
452
453     /* get the full pathname to the config file and try to open it */
454     configpath = ConfigGetUserConfigPath();
455     if (configpath == NULL)
456         return M64ERR_FILES;
457
458     filepath = combinepath(configpath, MUPEN64PLUS_CFG_NAME);
459     if (filepath == NULL)
460         return M64ERR_NO_MEMORY;
461
462     fPtr = fopen(filepath, "rb");
463     if (fPtr == NULL)
464     {
465         DebugMessage(M64MSG_INFO, "Couldn't open configuration file '%s'.  Using defaults.", filepath);
466         free(filepath);
467         l_SaveConfigOnExit = 1; /* auto-save the config file so that the defaults will be saved to disk */
468         return M64ERR_SUCCESS;
469     }
470     free(filepath);
471
472     /* read the entire config file */
473     fseek(fPtr, 0L, SEEK_END);
474     filelen = ftell(fPtr);
475     fseek(fPtr, 0L, SEEK_SET);
476
477     configtext = (char *) malloc(filelen + 1);
478     if (configtext == NULL)
479     {
480         fclose(fPtr);
481         return M64ERR_NO_MEMORY;
482     }
483     if (fread(configtext, 1, filelen, fPtr) != filelen)
484     {
485         free(configtext);
486         fclose(fPtr);
487         return M64ERR_FILES;
488     }
489     fclose(fPtr);
490
491     /* parse the file data */
492     current_section = NULL;
493     line = configtext;
494     end = configtext + filelen;
495     lastcomment = NULL;
496     *end = 0;
497     while (line < end)
498     {
499         ini_line l = ini_parse_line(&line);
500         switch (l.type)
501         {
502             case INI_COMMENT:
503                 lastcomment = l.value;
504                 break;
505
506             case INI_SECTION:
507                 rval = ConfigOpenSection(l.name, (m64p_handle *) &current_section);
508                 if (rval != M64ERR_SUCCESS)
509                 {
510                     free(configtext);
511                     return rval;
512                 }
513                 lastcomment = NULL;
514                 break;
515
516             case INI_PROPERTY:
517                 if (l.value[0] == '"' && l.value[strlen(l.value)-1] == '"')
518                 {
519                     l.value++;
520                     l.value[strlen(l.value)-1] = 0;
521                     ConfigSetDefaultString((m64p_handle) current_section, l.name, l.value, lastcomment);
522                 }
523                 else if (osal_insensitive_strcmp(l.value, "false") == 0)
524                 {
525                     ConfigSetDefaultBool((m64p_handle) current_section, l.name, 0, lastcomment);
526                 }
527                 else if (osal_insensitive_strcmp(l.value, "true") == 0)
528                 {
529                     ConfigSetDefaultBool((m64p_handle) current_section, l.name, 1, lastcomment);
530                 }
531                 else if (is_numeric(l.value))
532                 {
533                     int val_int = (int) strtol(l.value, NULL, 10);
534                     float val_float = (float) strtod(l.value, NULL);
535                     if ((val_float - val_int) != 0.0)
536                         ConfigSetDefaultFloat((m64p_handle) current_section, l.name, val_float, lastcomment);
537                     else
538                         ConfigSetDefaultInt((m64p_handle) current_section, l.name, val_int, lastcomment);
539                 }
540                 else
541                 {
542                     /* assume that it's a string */
543                     ConfigSetDefaultString((m64p_handle) current_section, l.name, l.value, lastcomment);
544                 }
545                 lastcomment = NULL;
546                 break;
547
548             default:
549                 break;
550         }
551     }
552
553     /* release memory used for config file text */
554     free(configtext);
555
556     /* duplicate the entire config data list, to store a copy of the list which represents the state of the file on disk */
557     copy_configlist_active_to_saved();
558
559     return M64ERR_SUCCESS;
560 }
561
562 m64p_error ConfigShutdown(void)
563 {
564     /* first, save the file if necessary */
565     if (l_SaveConfigOnExit)
566         ConfigSaveFile();
567
568     /* reset the initialized flag */
569     if (!l_ConfigInit)
570         return M64ERR_NOT_INIT;
571     l_ConfigInit = 0;
572
573     /* free any malloc'd local variables */
574     if (l_DataDirOverride != NULL)
575     {
576         free(l_DataDirOverride);
577         l_DataDirOverride = NULL;
578     }
579     if (l_ConfigDirOverride != NULL)
580     {
581         free(l_ConfigDirOverride);
582         l_ConfigDirOverride = NULL;
583     }
584
585     /* free all of the memory in the 2 lists */
586     delete_list(&l_ConfigListActive);
587     delete_list(&l_ConfigListSaved);
588
589     return M64ERR_SUCCESS;
590 }
591
592 /* ------------------------------------------------ */
593 /* Selector functions, exported outside of the Core */
594 /* ------------------------------------------------ */
595
596 EXPORT m64p_error CALL ConfigListSections(void *context, void (*SectionListCallback)(void * context, const char * SectionName))
597 {
598     config_section *curr_section;
599
600     if (!l_ConfigInit)
601         return M64ERR_NOT_INIT;
602     if (SectionListCallback == NULL)
603         return M64ERR_INPUT_ASSERT;
604
605     /* just walk through the section list, making a callback for each section name */
606     curr_section = l_ConfigListActive;
607     while (curr_section != NULL)
608     {
609         (*SectionListCallback)(context, curr_section->name);
610         curr_section = curr_section->next;
611     }
612
613     return M64ERR_SUCCESS;
614 }
615
616 EXPORT m64p_error CALL ConfigOpenSection(const char *SectionName, m64p_handle *ConfigSectionHandle)
617 {
618     config_section **curr_section;
619     config_section *new_section;
620
621     if (!l_ConfigInit)
622         return M64ERR_NOT_INIT;
623     if (SectionName == NULL || ConfigSectionHandle == NULL)
624         return M64ERR_INPUT_ASSERT;
625
626     /* walk through the section list, looking for a case-insensitive name match */
627     curr_section = find_alpha_section_link(&l_ConfigListActive, SectionName);
628     if (*curr_section != NULL && osal_insensitive_strcmp(SectionName, (*curr_section)->name) == 0)
629     {
630         *ConfigSectionHandle = *curr_section;
631         return M64ERR_SUCCESS;
632     }
633
634     /* didn't find the section, so create new one */
635     new_section = config_section_create(SectionName);
636     if (new_section == NULL)
637         return M64ERR_NO_MEMORY;
638
639     /* add section to list in alphabetical order */
640     new_section->next = *curr_section;
641     *curr_section = new_section;
642
643     *ConfigSectionHandle = new_section;
644     return M64ERR_SUCCESS;
645 }
646
647 EXPORT m64p_error CALL ConfigListParameters(m64p_handle ConfigSectionHandle, void *context, void (*ParameterListCallback)(void * context, const char *ParamName, m64p_type ParamType))
648 {
649     config_section *section;
650     config_var *curr_var;
651
652     if (!l_ConfigInit)
653         return M64ERR_NOT_INIT;
654     if (ConfigSectionHandle == NULL || ParameterListCallback == NULL)
655         return M64ERR_INPUT_ASSERT;
656
657     section = (config_section *) ConfigSectionHandle;
658     if (section->magic != SECTION_MAGIC)
659         return M64ERR_INPUT_INVALID;
660
661     /* walk through this section's parameter list, making a callback for each parameter */
662     curr_var = section->first_var;
663     while (curr_var != NULL)
664     {
665         (*ParameterListCallback)(context, curr_var->name, curr_var->type);
666         curr_var = curr_var->next;
667     }
668
669   return M64ERR_SUCCESS;
670 }
671
672 EXPORT int CALL ConfigHasUnsavedChanges(const char *SectionName)
673 {
674     config_section *input_section, *curr_section;
675     config_var *active_var, *saved_var;
676
677     /* check input conditions */
678     if (!l_ConfigInit)
679     {
680         DebugMessage(M64MSG_ERROR, "ConfigHasUnsavedChanges(): Core config not initialized!");
681         return 0;
682     }
683
684     /* if SectionName is NULL or blank, then check all sections */
685     if (SectionName == NULL || strlen(SectionName) < 1)
686     {
687         int iNumActiveSections = 0, iNumSavedSections = 0;
688         /* first, search through all sections in Active list.  Recursively call ourself and return 1 if changed */
689         curr_section = l_ConfigListActive;
690         while (curr_section != NULL)
691         {
692             if (ConfigHasUnsavedChanges(curr_section->name))
693                 return 1;
694             curr_section = curr_section->next;
695             iNumActiveSections++;
696         }
697         /* Next, count the number of Saved sections and see if the count matches */
698         curr_section = l_ConfigListSaved;
699         while (curr_section != NULL)
700         {
701             curr_section = curr_section->next;
702             iNumSavedSections++;
703         }
704         if (iNumActiveSections == iNumSavedSections)
705             return 0;  /* no changes */
706         else
707             return 1;
708     }
709
710     /* walk through the Active section list, looking for a case-insensitive name match with input string */
711     input_section = find_section(l_ConfigListActive, SectionName);
712     if (input_section == NULL)
713     {
714         DebugMessage(M64MSG_ERROR, "ConfigHasUnsavedChanges(): section name '%s' not found!", SectionName);
715         return 0;
716     }
717
718     /* walk through the Saved section list, looking for a case-insensitive name match */
719     curr_section = find_section(l_ConfigListSaved, SectionName);
720     if (curr_section == NULL)
721     {
722         /* if this section isn't present in saved list, then it has been newly created */
723         return 1;
724     }
725
726     /* compare all of the variables in the two sections. They are expected to be in the same order */
727     active_var = input_section->first_var;
728     saved_var = curr_section->first_var;
729     while (active_var != NULL && saved_var != NULL)
730     {
731         if (strcmp(active_var->name, saved_var->name) != 0)
732             return 1;
733         if (active_var->type != saved_var->type)
734             return 1;
735         switch(active_var->type)
736         {
737             case M64TYPE_INT:
738                 if (active_var->val.integer != saved_var->val.integer)
739                     return 1;
740                 break;
741             case M64TYPE_FLOAT:
742                 if (active_var->val.number != saved_var->val.number)
743                     return 1;
744                 break;
745             case M64TYPE_BOOL:
746                 if ((active_var->val.integer != 0) != (saved_var->val.integer != 0))
747                     return 1;
748                 break;
749             case M64TYPE_STRING:
750                 if (active_var->val.string == NULL)
751                 {
752                     DebugMessage(M64MSG_ERROR, "ConfigHasUnsavedChanges(): Variable '%s' NULL Active string pointer!", active_var->name);
753                     return 1;
754                 }
755                 if (saved_var->val.string == NULL)
756                 {
757                     DebugMessage(M64MSG_ERROR, "ConfigHasUnsavedChanges(): Variable '%s' NULL Saved string pointer!", active_var->name);
758                     return 1;
759                 }
760                 if (strcmp(active_var->val.string, saved_var->val.string) != 0)
761                     return 1;
762                 break;
763             default:
764                 DebugMessage(M64MSG_ERROR, "ConfigHasUnsavedChanges(): Invalid variable '%s' type %i!", active_var->name, active_var->type);
765                 return 1;
766         }
767         if (active_var->comment != NULL && saved_var->comment != NULL && strcmp(active_var->comment, saved_var->comment) != 0)
768             return 1;
769         active_var = active_var->next;
770         saved_var = saved_var->next;
771     }
772
773     /* any extra new variables on the end, or deleted variables? */
774     if (active_var != NULL || saved_var != NULL)
775         return 1;
776
777     /* exactly the same */
778     return 0;
779 }
780
781 /* ------------------------------------------------------- */
782 /* Modifier functions, exported outside of the Core        */
783 /* ------------------------------------------------------- */
784
785 EXPORT m64p_error CALL ConfigDeleteSection(const char *SectionName)
786 {
787     config_section **curr_section_link;
788     config_section *next_section;
789
790     if (!l_ConfigInit)
791         return M64ERR_NOT_INIT;
792     if (l_ConfigListActive == NULL)
793         return M64ERR_INPUT_NOT_FOUND;
794
795     /* find the named section and pull it out of the list */
796     curr_section_link = find_section_link(&l_ConfigListActive, SectionName);
797     if (*curr_section_link == NULL)
798         return M64ERR_INPUT_NOT_FOUND;
799
800     next_section = (*curr_section_link)->next;
801
802     /* delete the named section */
803     delete_section(*curr_section_link);
804
805     /* fix the pointer to point to the next section after the deleted one */
806     *curr_section_link = next_section;
807
808     return M64ERR_SUCCESS;
809 }
810
811 EXPORT m64p_error CALL ConfigSaveFile(void)
812 {
813     if (!l_ConfigInit)
814         return M64ERR_NOT_INIT;
815
816     /* copy the active config list to the saved config list */
817     copy_configlist_active_to_saved();
818
819     /* write the saved config list out to a file */
820     return (write_configlist_file());
821 }
822
823 EXPORT m64p_error CALL ConfigSaveSection(const char *SectionName)
824 {
825     config_section *curr_section, *new_section;
826     config_section **insertion_point;
827
828     if (!l_ConfigInit)
829         return M64ERR_NOT_INIT;
830     if (SectionName == NULL || strlen(SectionName) < 1)
831         return M64ERR_INPUT_ASSERT;
832
833     /* walk through the Active section list, looking for a case-insensitive name match */
834     curr_section = find_section(l_ConfigListActive, SectionName);
835     if (curr_section == NULL)
836         return M64ERR_INPUT_NOT_FOUND;
837
838     /* duplicate this section */
839     new_section = section_deepcopy(curr_section);
840     if (new_section == NULL)
841         return M64ERR_NO_MEMORY;
842
843     /* update config section that's in the Saved list with the new one */
844     insertion_point = find_alpha_section_link(&l_ConfigListSaved, SectionName);
845     if (*insertion_point != NULL && osal_insensitive_strcmp((*insertion_point)->name, SectionName) == 0)
846     {
847         /* the section exists in the saved list and will be replaced */
848         new_section->next = (*insertion_point)->next;
849         delete_section(*insertion_point);
850         *insertion_point = new_section;
851     }
852     else
853     {
854         /* the section didn't exist in the saved list and has to be inserted */
855         new_section->next = *insertion_point;
856         *insertion_point = new_section;
857     }
858
859     /* write the saved config list out to a file */
860     return (write_configlist_file());
861 }
862
863 EXPORT m64p_error CALL ConfigRevertChanges(const char *SectionName)
864 {
865     config_section **active_section_link, *active_section, *saved_section, *new_section;
866
867     /* check input conditions */
868     if (!l_ConfigInit)
869         return M64ERR_NOT_INIT;
870     if (SectionName == NULL)
871         return M64ERR_INPUT_ASSERT;
872
873     /* walk through the Active section list, looking for a case-insensitive name match with input string */
874     active_section_link = find_section_link(&l_ConfigListActive, SectionName);
875     active_section = *active_section_link;
876     if (active_section == NULL)
877         return M64ERR_INPUT_NOT_FOUND;
878
879     /* walk through the Saved section list, looking for a case-insensitive name match */
880     saved_section = find_section(l_ConfigListSaved, SectionName);
881     if (saved_section == NULL)
882     {
883         /* if this section isn't present in saved list, then it has been newly created */
884         return M64ERR_INPUT_NOT_FOUND;
885     }
886
887     /* copy the section as it is on the disk */
888     new_section = section_deepcopy(saved_section);
889     if (new_section == NULL)
890         return M64ERR_NO_MEMORY;
891
892     /* replace active_section with saved_section in the linked list */
893     *active_section_link = new_section;
894     new_section->next = active_section->next;
895
896     /* release memory associated with active_section */
897     delete_section(active_section);
898
899     return M64ERR_SUCCESS;
900 }
901
902
903 /* ------------------------------------------------------- */
904 /* Generic Get/Set functions, exported outside of the Core */
905 /* ------------------------------------------------------- */
906
907 EXPORT m64p_error CALL ConfigSetParameter(m64p_handle ConfigSectionHandle, const char *ParamName, m64p_type ParamType, const void *ParamValue)
908 {
909     config_section *section;
910     config_var *var;
911
912     /* check input conditions */
913     if (!l_ConfigInit)
914         return M64ERR_NOT_INIT;
915     if (ConfigSectionHandle == NULL || ParamName == NULL || ParamValue == NULL || (int) ParamType < 1 || (int) ParamType > 4)
916         return M64ERR_INPUT_ASSERT;
917
918     section = (config_section *) ConfigSectionHandle;
919     if (section->magic != SECTION_MAGIC)
920         return M64ERR_INPUT_INVALID;
921
922     /* if this parameter doesn't already exist, then create it and add it to the section */
923     var = find_section_var(section, ParamName);
924     if (var == NULL)
925     {
926         var = config_var_create(ParamName, NULL);
927         if (var == NULL)
928             return M64ERR_NO_MEMORY;
929         append_var_to_section(section, var);
930     }
931
932     /* cleanup old values */
933     switch (var->type)
934     {
935         case M64TYPE_STRING:
936             free(var->val.string);
937             break;
938         default:
939             break;
940     }
941
942     /* set this parameter's value */
943     var->type = ParamType;
944     switch(ParamType)
945     {
946         case M64TYPE_INT:
947             var->val.integer = *((int *) ParamValue);
948             break;
949         case M64TYPE_FLOAT:
950             var->val.number = *((float *) ParamValue);
951             break;
952         case M64TYPE_BOOL:
953             var->val.integer = (*((int *) ParamValue) != 0);
954             break;
955         case M64TYPE_STRING:
956             var->val.string = strdup((char *)ParamValue);
957             if (var->val.string == NULL)
958                 return M64ERR_NO_MEMORY;
959             break;
960         default:
961             /* this is logically impossible because of the ParamType check at the top of this function */
962             break;
963     }
964
965     return M64ERR_SUCCESS;
966 }
967
968 EXPORT m64p_error CALL ConfigGetParameter(m64p_handle ConfigSectionHandle, const char *ParamName, m64p_type ParamType, void *ParamValue, int MaxSize)
969 {
970     config_section *section;
971     config_var *var;
972
973     /* check input conditions */
974     if (!l_ConfigInit)
975         return M64ERR_NOT_INIT;
976     if (ConfigSectionHandle == NULL || ParamName == NULL || ParamValue == NULL || (int) ParamType < 1 || (int) ParamType > 4)
977         return M64ERR_INPUT_ASSERT;
978
979     section = (config_section *) ConfigSectionHandle;
980     if (section->magic != SECTION_MAGIC)
981         return M64ERR_INPUT_INVALID;
982
983     /* if this parameter doesn't already exist, return an error */
984     var = find_section_var(section, ParamName);
985     if (var == NULL)
986         return M64ERR_INPUT_NOT_FOUND;
987
988     /* call the specific Get function to translate the parameter to the desired type */
989     switch(ParamType)
990     {
991         case M64TYPE_INT:
992             if (MaxSize < sizeof(int)) return M64ERR_INPUT_INVALID;
993             if (var->type != M64TYPE_INT && var->type != M64TYPE_FLOAT) return M64ERR_WRONG_TYPE;
994             *((int *) ParamValue) = ConfigGetParamInt(ConfigSectionHandle, ParamName);
995             break;
996         case M64TYPE_FLOAT:
997             if (MaxSize < sizeof(float)) return M64ERR_INPUT_INVALID;
998             if (var->type != M64TYPE_INT && var->type != M64TYPE_FLOAT) return M64ERR_WRONG_TYPE;
999             *((float *) ParamValue) = ConfigGetParamFloat(ConfigSectionHandle, ParamName);
1000             break;
1001         case M64TYPE_BOOL:
1002             if (MaxSize < sizeof(int)) return M64ERR_INPUT_INVALID;
1003             if (var->type != M64TYPE_BOOL && var->type != M64TYPE_INT) return M64ERR_WRONG_TYPE;
1004             *((int *) ParamValue) = ConfigGetParamBool(ConfigSectionHandle, ParamName);
1005             break;
1006         case M64TYPE_STRING:
1007         {
1008             const char *string;
1009             if (MaxSize < 1) return M64ERR_INPUT_INVALID;
1010             if (var->type != M64TYPE_STRING && var->type != M64TYPE_BOOL) return M64ERR_WRONG_TYPE;
1011             string = ConfigGetParamString(ConfigSectionHandle, ParamName);
1012             strncpy((char *) ParamValue, string, MaxSize);
1013             *((char *) ParamValue + MaxSize - 1) = 0;
1014             break;
1015         }
1016         default:
1017             /* this is logically impossible because of the ParamType check at the top of this function */
1018             break;
1019     }
1020
1021     return M64ERR_SUCCESS;
1022 }
1023
1024 EXPORT m64p_error CALL ConfigGetParameterType(m64p_handle ConfigSectionHandle, const char *ParamName, m64p_type *ParamType)
1025 {
1026     config_section *section;
1027     config_var *var;
1028
1029     /* check input conditions */
1030     if (!l_ConfigInit)
1031         return M64ERR_NOT_INIT;
1032     if (ConfigSectionHandle == NULL || ParamName == NULL || ParamType == NULL)
1033         return M64ERR_INPUT_ASSERT;
1034
1035     section = (config_section *) ConfigSectionHandle;
1036     if (section->magic != SECTION_MAGIC)
1037         return M64ERR_INPUT_INVALID;
1038
1039     /* if this parameter doesn't already exist, return an error */
1040     var = find_section_var(section, ParamName);
1041     if (var == NULL)
1042         return M64ERR_INPUT_NOT_FOUND;
1043
1044     *ParamType = var->type;
1045     return M64ERR_SUCCESS;
1046 }
1047
1048
1049 EXPORT const char * CALL ConfigGetParameterHelp(m64p_handle ConfigSectionHandle, const char *ParamName)
1050 {
1051     config_section *section;
1052     config_var *var;
1053
1054     /* check input conditions */
1055     if (!l_ConfigInit || ConfigSectionHandle == NULL || ParamName == NULL)
1056         return NULL;
1057
1058     section = (config_section *) ConfigSectionHandle;
1059     if (section->magic != SECTION_MAGIC)
1060         return NULL;
1061
1062     /* if this parameter doesn't exist, return an error */
1063     var = find_section_var(section, ParamName);
1064     if (var == NULL)
1065         return NULL;
1066
1067     return var->comment;
1068 }
1069
1070 /* ------------------------------------------------------- */
1071 /* Special Get/Set functions, exported outside of the Core */
1072 /* ------------------------------------------------------- */
1073
1074 EXPORT m64p_error CALL ConfigSetDefaultInt(m64p_handle ConfigSectionHandle, const char *ParamName, int ParamValue, const char *ParamHelp)
1075 {
1076     config_section *section;
1077     config_var *var;
1078
1079     /* check input conditions */
1080     if (!l_ConfigInit)
1081         return M64ERR_NOT_INIT;
1082     if (ConfigSectionHandle == NULL || ParamName == NULL)
1083         return M64ERR_INPUT_ASSERT;
1084
1085     section = (config_section *) ConfigSectionHandle;
1086     if (section->magic != SECTION_MAGIC)
1087         return M64ERR_INPUT_INVALID;
1088
1089     /* if this parameter already exists, then just return successfully */
1090     var = find_section_var(section, ParamName);
1091     if (var != NULL)
1092         return M64ERR_SUCCESS;
1093
1094     /* otherwise create a new config_var object and add it to this section */
1095     var = config_var_create(ParamName, ParamHelp);
1096     if (var == NULL)
1097         return M64ERR_NO_MEMORY;
1098     var->type = M64TYPE_INT;
1099     var->val.integer = ParamValue;
1100     append_var_to_section(section, var);
1101
1102     return M64ERR_SUCCESS;
1103 }
1104
1105 EXPORT m64p_error CALL ConfigSetDefaultFloat(m64p_handle ConfigSectionHandle, const char *ParamName, float ParamValue, const char *ParamHelp)
1106 {
1107     config_section *section;
1108     config_var *var;
1109
1110     /* check input conditions */
1111     if (!l_ConfigInit)
1112         return M64ERR_NOT_INIT;
1113     if (ConfigSectionHandle == NULL || ParamName == NULL)
1114         return M64ERR_INPUT_ASSERT;
1115
1116     section = (config_section *) ConfigSectionHandle;
1117     if (section->magic != SECTION_MAGIC)
1118         return M64ERR_INPUT_INVALID;
1119
1120     /* if this parameter already exists, then just return successfully */
1121     var = find_section_var(section, ParamName);
1122     if (var != NULL)
1123         return M64ERR_SUCCESS;
1124
1125     /* otherwise create a new config_var object and add it to this section */
1126     var = config_var_create(ParamName, ParamHelp);
1127     if (var == NULL)
1128         return M64ERR_NO_MEMORY;
1129     var->type = M64TYPE_FLOAT;
1130     var->val.number = ParamValue;
1131     append_var_to_section(section, var);
1132
1133     return M64ERR_SUCCESS;
1134 }
1135
1136 EXPORT m64p_error CALL ConfigSetDefaultBool(m64p_handle ConfigSectionHandle, const char *ParamName, int ParamValue, const char *ParamHelp)
1137 {
1138     config_section *section;
1139     config_var *var;
1140
1141     /* check input conditions */
1142     if (!l_ConfigInit)
1143         return M64ERR_NOT_INIT;
1144     if (ConfigSectionHandle == NULL || ParamName == NULL)
1145         return M64ERR_INPUT_ASSERT;
1146
1147     section = (config_section *) ConfigSectionHandle;
1148     if (section->magic != SECTION_MAGIC)
1149         return M64ERR_INPUT_INVALID;
1150
1151     /* if this parameter already exists, then just return successfully */
1152     var = find_section_var(section, ParamName);
1153     if (var != NULL)
1154         return M64ERR_SUCCESS;
1155
1156     /* otherwise create a new config_var object and add it to this section */
1157     var = config_var_create(ParamName, ParamHelp);
1158     if (var == NULL)
1159         return M64ERR_NO_MEMORY;
1160     var->type = M64TYPE_BOOL;
1161     var->val.integer = ParamValue ? 1 : 0;
1162     append_var_to_section(section, var);
1163
1164     return M64ERR_SUCCESS;
1165 }
1166
1167 EXPORT m64p_error CALL ConfigSetDefaultString(m64p_handle ConfigSectionHandle, const char *ParamName, const char * ParamValue, const char *ParamHelp)
1168 {
1169     config_section *section;
1170     config_var *var;
1171
1172     /* check input conditions */
1173     if (!l_ConfigInit)
1174         return M64ERR_NOT_INIT;
1175     if (ConfigSectionHandle == NULL || ParamName == NULL || ParamValue == NULL)
1176         return M64ERR_INPUT_ASSERT;
1177
1178     section = (config_section *) ConfigSectionHandle;
1179     if (section->magic != SECTION_MAGIC)
1180         return M64ERR_INPUT_INVALID;
1181
1182     /* if this parameter already exists, then just return successfully */
1183     var = find_section_var(section, ParamName);
1184     if (var != NULL)
1185         return M64ERR_SUCCESS;
1186
1187     /* otherwise create a new config_var object and add it to this section */
1188     var = config_var_create(ParamName, ParamHelp);
1189     if (var == NULL)
1190         return M64ERR_NO_MEMORY;
1191     var->type = M64TYPE_STRING;
1192     var->val.string = strdup(ParamValue);
1193     if (var->val.string == NULL)
1194     {
1195         delete_var(var);
1196         return M64ERR_NO_MEMORY;
1197     }
1198     append_var_to_section(section, var);
1199
1200     return M64ERR_SUCCESS;
1201 }
1202
1203 EXPORT int CALL ConfigGetParamInt(m64p_handle ConfigSectionHandle, const char *ParamName)
1204 {
1205     config_section *section;
1206     config_var *var;
1207
1208     /* check input conditions */
1209     if (!l_ConfigInit || ConfigSectionHandle == NULL || ParamName == NULL)
1210     {
1211         DebugMessage(M64MSG_ERROR, "ConfigGetParamInt(): Input assertion!");
1212         return 0;
1213     }
1214
1215     section = (config_section *) ConfigSectionHandle;
1216     if (section->magic != SECTION_MAGIC)
1217     {
1218         DebugMessage(M64MSG_ERROR, "ConfigGetParamInt(): ConfigSectionHandle invalid!");
1219         return 0;
1220     }
1221
1222     /* if this parameter doesn't already exist, return an error */
1223     var = find_section_var(section, ParamName);
1224     if (var == NULL)
1225     {
1226         DebugMessage(M64MSG_ERROR, "ConfigGetParamInt(): Parameter '%s' not found!", ParamName);
1227         return 0;
1228     }
1229
1230     /* translate the actual variable type to an int */
1231     switch(var->type)
1232     {
1233         case M64TYPE_INT:
1234             return var->val.integer;
1235         case M64TYPE_FLOAT:
1236             return (int) var->val.number;
1237         case M64TYPE_BOOL:
1238             return (var->val.integer != 0);
1239         case M64TYPE_STRING:
1240             return atoi(var->val.string);
1241         default:
1242             DebugMessage(M64MSG_ERROR, "ConfigGetParamInt(): invalid internal parameter type for '%s'", ParamName);
1243             return 0;
1244     }
1245
1246     return 0;
1247 }
1248
1249 EXPORT float CALL ConfigGetParamFloat(m64p_handle ConfigSectionHandle, const char *ParamName)
1250 {
1251     config_section *section;
1252     config_var *var;
1253
1254     /* check input conditions */
1255     if (!l_ConfigInit || ConfigSectionHandle == NULL || ParamName == NULL)
1256     {
1257         DebugMessage(M64MSG_ERROR, "ConfigGetParamFloat(): Input assertion!");
1258         return 0.0;
1259     }
1260
1261     section = (config_section *) ConfigSectionHandle;
1262     if (section->magic != SECTION_MAGIC)
1263     {
1264         DebugMessage(M64MSG_ERROR, "ConfigGetParamFloat(): ConfigSectionHandle invalid!");
1265         return 0.0;
1266     }
1267
1268     /* if this parameter doesn't already exist, return an error */
1269     var = find_section_var(section, ParamName);
1270     if (var == NULL)
1271     {
1272         DebugMessage(M64MSG_ERROR, "ConfigGetParamFloat(): Parameter '%s' not found!", ParamName);
1273         return 0.0;
1274     }
1275
1276     /* translate the actual variable type to an int */
1277     switch(var->type)
1278     {
1279         case M64TYPE_INT:
1280             return (float) var->val.integer;
1281         case M64TYPE_FLOAT:
1282             return var->val.number;
1283         case M64TYPE_BOOL:
1284             return (var->val.integer != 0) ? 1.0f : 0.0f;
1285         case M64TYPE_STRING:
1286             return (float) atof(var->val.string);
1287         default:
1288             DebugMessage(M64MSG_ERROR, "ConfigGetParamFloat(): invalid internal parameter type for '%s'", ParamName);
1289             return 0.0;
1290     }
1291
1292     return 0.0;
1293 }
1294
1295 EXPORT int CALL ConfigGetParamBool(m64p_handle ConfigSectionHandle, const char *ParamName)
1296 {
1297     config_section *section;
1298     config_var *var;
1299
1300     /* check input conditions */
1301     if (!l_ConfigInit || ConfigSectionHandle == NULL || ParamName == NULL)
1302     {
1303         DebugMessage(M64MSG_ERROR, "ConfigGetParamBool(): Input assertion!");
1304         return 0;
1305     }
1306
1307     section = (config_section *) ConfigSectionHandle;
1308     if (section->magic != SECTION_MAGIC)
1309     {
1310         DebugMessage(M64MSG_ERROR, "ConfigGetParamBool(): ConfigSectionHandle invalid!");
1311         return 0;
1312     }
1313
1314     /* if this parameter doesn't already exist, return an error */
1315     var = find_section_var(section, ParamName);
1316     if (var == NULL)
1317     {
1318         DebugMessage(M64MSG_ERROR, "ConfigGetParamBool(): Parameter '%s' not found!", ParamName);
1319         return 0;
1320     }
1321
1322     /* translate the actual variable type to an int */
1323     switch(var->type)
1324     {
1325         case M64TYPE_INT:
1326             return (var->val.integer != 0);
1327         case M64TYPE_FLOAT:
1328             return (var->val.number != 0.0);
1329         case M64TYPE_BOOL:
1330             return var->val.integer;
1331         case M64TYPE_STRING:
1332             return (osal_insensitive_strcmp(var->val.string, "true") == 0);
1333         default:
1334             DebugMessage(M64MSG_ERROR, "ConfigGetParamBool(): invalid internal parameter type for '%s'", ParamName);
1335             return 0;
1336     }
1337
1338     return 0;
1339 }
1340
1341 EXPORT const char * CALL ConfigGetParamString(m64p_handle ConfigSectionHandle, const char *ParamName)
1342 {
1343     static char outstr[64];  /* warning: not thread safe */
1344     config_section *section;
1345     config_var *var;
1346
1347     /* check input conditions */
1348     if (!l_ConfigInit || ConfigSectionHandle == NULL || ParamName == NULL)
1349     {
1350         DebugMessage(M64MSG_ERROR, "ConfigGetParamString(): Input assertion!");
1351         return "";
1352     }
1353
1354     section = (config_section *) ConfigSectionHandle;
1355     if (section->magic != SECTION_MAGIC)
1356     {
1357         DebugMessage(M64MSG_ERROR, "ConfigGetParamString(): ConfigSectionHandle invalid!");
1358         return "";
1359     }
1360
1361     /* if this parameter doesn't already exist, return an error */
1362     var = find_section_var(section, ParamName);
1363     if (var == NULL)
1364     {
1365         DebugMessage(M64MSG_ERROR, "ConfigGetParamString(): Parameter '%s' not found!", ParamName);
1366         return "";
1367     }
1368
1369     /* translate the actual variable type to an int */
1370     switch(var->type)
1371     {
1372         case M64TYPE_INT:
1373             snprintf(outstr, 63, "%i", var->val.integer);
1374             outstr[63] = 0;
1375             return outstr;
1376         case M64TYPE_FLOAT:
1377             snprintf(outstr, 63, "%f", var->val.number);
1378             outstr[63] = 0;
1379             return outstr;
1380         case M64TYPE_BOOL:
1381             return (var->val.integer ? "True" : "False");
1382         case M64TYPE_STRING:
1383             return var->val.string;
1384         default:
1385             DebugMessage(M64MSG_ERROR, "ConfigGetParamString(): invalid internal parameter type for '%s'", ParamName);
1386             return "";
1387     }
1388
1389   return "";
1390 }
1391
1392 /* ------------------------------------------------------ */
1393 /* OS Abstraction functions, exported outside of the Core */
1394 /* ------------------------------------------------------ */
1395
1396 EXPORT const char * CALL ConfigGetSharedDataFilepath(const char *filename)
1397 {
1398     const char *configsharepath = NULL;
1399     m64p_handle CoreHandle = NULL;
1400
1401     /* check input parameter */
1402     if (filename == NULL) return NULL;
1403
1404     /* try to get the SharedDataPath string variable in the Core configuration section */
1405     if (ConfigOpenSection("Core", &CoreHandle) == M64ERR_SUCCESS)
1406     {
1407         configsharepath = ConfigGetParamString(CoreHandle, "SharedDataPath");
1408     }
1409
1410     return osal_get_shared_filepath(filename, l_DataDirOverride, configsharepath);
1411 }
1412
1413 EXPORT const char * CALL ConfigGetUserConfigPath(void)
1414 {
1415     if (l_ConfigDirOverride != NULL)
1416     {
1417         osal_mkdirp(l_ConfigDirOverride, 0700);
1418         return l_ConfigDirOverride;
1419     }
1420     else
1421         return osal_get_user_configpath();
1422 }
1423
1424 EXPORT const char * CALL ConfigGetUserDataPath(void)
1425 {
1426   return osal_get_user_datapath();
1427 }
1428
1429 EXPORT const char * CALL ConfigGetUserCachePath(void)
1430 {
1431   return osal_get_user_cachepath();
1432 }
1433