1 /** @file con_config.cpp  Config file IO.
2  *
3  * @authors Copyright © 2003-2017 Jaakko Keränen <jaakko.keranen@iki.fi>
4  * @authors Copyright © 2006-2014 Daniel Swanson <danij@dengine.net>
5  *
6  * @par License
7  * GPL: http://www.gnu.org/licenses/gpl.html
8  *
9  * <small>This program is free software; you can redistribute it and/or modify
10  * it under the terms of the GNU General Public License as published by the
11  * Free Software Foundation; either version 2 of the License, or (at your
12  * option) any later version. This program is distributed in the hope that it
13  * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
15  * Public License for more details. You should have received a copy of the GNU
16  * General Public License along with this program; if not, see:
17  * http://www.gnu.org/licenses</small>
18  */
19 
20 #include "con_config.h"
21 #include "ui/inputsystem.h"
22 
23 #include <de/App>
24 #include <de/DirectoryFeed>
25 #include <de/FileSystem>
26 #include <de/Log>
27 #include <de/NativeFile>
28 #include <de/Path>
29 #include <de/Writer>
30 #include <de/c_wrapper.h>
31 #include <cctype>
32 
33 #include <doomsday/help.h>
34 #include <doomsday/console/exec.h>
35 #include <doomsday/console/var.h>
36 #include <doomsday/console/alias.h>
37 #include <doomsday/console/knownword.h>
38 #include <doomsday/filesys/fs_main.h>
39 #include <doomsday/filesys/fs_util.h>
40 #include <doomsday/Games>
41 
42 #include "dd_main.h"
43 #include "dd_def.h"
44 
45 #ifdef __CLIENT__
46 #  include "clientapp.h"
47 
48 #  include "world/p_players.h"
49 
50 #  include "BindContext"
51 #  include "CommandBinding"
52 #  include "ImpulseBinding"
53 #endif
54 
55 using namespace de;
56 
57 static Path cfgFile;
58 static int flagsAllow;
59 
60 static String const STR_COMMENT = "# ";
61 
writeHeaderComment(de::Writer & out)62 static void writeHeaderComment(de::Writer &out)
63 {
64     if (!App_GameLoaded())
65     {
66         out.writeText("# " DOOMSDAY_NICENAME " " DOOMSDAY_VERSION_TEXT "\n");
67     }
68     else
69     {
70         out.writeText(String::format("# %s %s / " DOOMSDAY_NICENAME " " DOOMSDAY_VERSION_TEXT "\n",
71                 (char const *) gx.GetPointer(DD_PLUGIN_NAME),
72                 (char const *) gx.GetPointer(DD_PLUGIN_VERSION_SHORT)));
73     }
74 
75     out.writeText("# This configuration file is generated automatically. Each line is a\n"
76                   "# console command. Lines beginning with # are comments. Use autoexec.cfg\n"
77                   "# for your own startup commands.\n\n");
78 }
79 
writeVariableToFileWorker(knownword_t const * word,void * context)80 static int writeVariableToFileWorker(knownword_t const *word, void *context)
81 {
82     de::Writer *out = reinterpret_cast<de::Writer *>(context);
83     DENG_ASSERT(out != 0);
84 
85     cvar_t *var = (cvar_t *)word->data;
86     DENG2_ASSERT(var != 0);
87 
88     // Don't archive this cvar?
89     if (var->flags & CVF_NO_ARCHIVE)
90         return 0;
91 
92     AutoStr const *path = CVar_ComposePath(var);
93 
94     // First print the comment (help text).
95     if (char const *str = DH_GetString(DH_Find(Str_Text(path)), HST_DESCRIPTION))
96     {
97         out->writeText(String(str).addLinePrefix(STR_COMMENT) + "\n");
98     }
99 
100     out->writeText(String::format("%s %s", Str_Text(path),
101                                   var->flags & CVF_PROTECTED? "force " : ""));
102 
103     switch (var->type)
104     {
105     case CVT_BYTE:  out->writeText(String::format("%d", *(byte *) var->ptr)); break;
106     case CVT_INT:   out->writeText(String::format("%d", *(int *) var->ptr)); break;
107     case CVT_FLOAT: out->writeText(String::format("%s", M_TrimmedFloat(*(float *) var->ptr))); break;
108 
109     case CVT_CHARPTR:
110         out->writeText("\"");
111         if (CV_CHARPTR(var))
112         {
113             out->writeText(String(CV_CHARPTR(var)).escaped());
114         }
115         out->writeText("\"");
116         break;
117 
118     case CVT_URIPTR:
119         out->writeText("\"");
120         if (CV_URIPTR(var))
121         {
122             out->writeText(CV_URIPTR(var)->compose().escaped());
123         }
124         out->writeText("\"");
125         break;
126 
127     default:
128         break;
129     }
130     out->writeText("\n\n");
131 
132     return 0; // Continue iteration.
133 }
134 
writeVariablesToFile(de::Writer & out)135 static void writeVariablesToFile(de::Writer &out)
136 {
137     Con_IterateKnownWords(0, WT_CVAR, writeVariableToFileWorker, &out);
138 }
139 
writeAliasToFileWorker(knownword_t const * word,void * context)140 static int writeAliasToFileWorker(knownword_t const *word, void *context)
141 {
142     de::Writer *out = reinterpret_cast<de::Writer *>(context);
143     DENG2_ASSERT(out != 0);
144 
145     calias_t *cal = (calias_t *) word->data;
146     DENG2_ASSERT(cal != 0);
147 
148     out->writeText(String::format("alias \"%s\" \"%s\"\n",
149                                   String(cal->name).escaped().toUtf8().constData(),
150                                   String(cal->command).escaped().toUtf8().constData()));
151 
152     return 0; // Continue iteration.
153 }
154 
writeAliasesToFile(de::Writer & out)155 static void writeAliasesToFile(de::Writer &out)
156 {
157     Con_IterateKnownWords(0, WT_CALIAS, writeAliasToFileWorker, &out);
158 }
159 
writeConsoleState(Path const & filePath)160 static bool writeConsoleState(Path const &filePath)
161 {
162     if (filePath.isEmpty()) return false;
163 
164     // Ensure the destination directory exists.
165     String fileDir = filePath.toString().fileNamePath();
166     if (!fileDir.isEmpty())
167     {
168         F_MakePath(fileDir.toUtf8());
169     }
170 
171     try
172     {
173         File &file = App::rootFolder().replaceFile(filePath);
174         de::Writer out(file);
175 
176         LOG_SCR_VERBOSE("Writing console state to %s...") << file.description();
177 
178         writeHeaderComment(out);
179         out.writeText("#\n# CONSOLE VARIABLES\n#\n\n");
180         writeVariablesToFile(out);
181 
182         out.writeText("\n#\n# ALIASES\n#\n\n");
183         writeAliasesToFile(out);
184 
185         file.flush();
186     }
187     catch (Error const &er)
188     {
189         LOG_SCR_WARNING("Failed to open \"%s\" for writing: %s")
190                 << filePath << er.asText();
191         return false;
192     }
193     return true;
194 }
195 
196 #ifdef __CLIENT__
writeBindingsState(Path const & filePath)197 static bool writeBindingsState(Path const &filePath)
198 {
199     if (filePath.isEmpty()) return false;
200 
201     // Ensure the destination directory exists.
202     String fileDir = filePath.toString().fileNamePath();
203     if (!fileDir.isEmpty())
204     {
205         F_MakePath(fileDir.toUtf8());
206     }
207 
208     try
209     {
210         File &file = App::rootFolder().replaceFile(filePath);
211         de::Writer out(file);
212 
213         InputSystem &isys = ClientApp::inputSystem();
214 
215         LOG_SCR_VERBOSE("Writing input bindings to %s...") << file.description();
216 
217         writeHeaderComment(out);
218 
219         // Start with a clean slate when restoring the bindings.
220         out.writeText("clearbindings\n\n");
221 
222         isys.forAllContexts([&isys, &out] (BindContext &context)
223         {
224             // Commands.
225             context.forAllCommandBindings([&out, &context] (Record &rec)
226             {
227                 CommandBinding bind(rec);
228                 out.writeText(String::format("bindevent \"%s:%s\" \"",
229                                              context.name().toUtf8().constData(),
230                                              bind.composeDescriptor().toUtf8().constData()) +
231                               bind.gets("command").escaped() + "\"\n");
232                 return LoopContinue;
233             });
234 
235             // Impulses.
236             context.forAllImpulseBindings([&out, &context] (CompiledImpulseBindingRecord &rec)
237             {
238                 ImpulseBinding bind(rec);
239                 PlayerImpulse const *impulse = P_PlayerImpulsePtr(rec.compiled().impulseId);
240                 DENG2_ASSERT(impulse);
241 
242                 out.writeText(String::format("bindcontrol local%i-%s \"%s\"\n",
243                               bind.geti("localPlayer") + 1,
244                               impulse->name.toUtf8().constData(),
245                               bind.composeDescriptor().toUtf8().constData()));
246                 return LoopContinue;
247             });
248 
249             return LoopContinue;
250         });
251 
252         file.flush();
253         return true;
254     }
255     catch (Error const &er)
256     {
257         LOG_SCR_WARNING("Failed opening \"%s\" for writing: %s")
258                 << filePath << er.asText();
259     }
260     return false;
261 }
262 #endif // __CLIENT__
263 
writeState(Path const & filePath,Path const & bindingsFileName="")264 static bool writeState(Path const &filePath, Path const &bindingsFileName = "")
265 {
266     if (!filePath.isEmpty() && (flagsAllow & CPCF_ALLOW_SAVE_STATE))
267     {
268         writeConsoleState(filePath);
269     }
270 #ifdef __CLIENT__
271     if (!bindingsFileName.isEmpty() && (flagsAllow & CPCF_ALLOW_SAVE_BINDINGS))
272     {
273         // Bindings go into a separate file.
274         writeBindingsState(bindingsFileName);
275     }
276 #else
277     DENG2_UNUSED(bindingsFileName);
278 #endif
279     return true;
280 }
281 
Con_SetAllowed(int flags)282 void Con_SetAllowed(int flags)
283 {
284     if (flags != 0)
285     {
286         flagsAllow |= flags & (CPCF_ALLOW_SAVE_STATE | CPCF_ALLOW_SAVE_BINDINGS);
287     }
288     else
289     {
290         flagsAllow = 0;
291     }
292 }
293 
Con_ParseCommands(File const & file,int flags)294 bool Con_ParseCommands(File const &file, int flags)
295 {
296     LOG_SCR_MSG("Parsing console commands in %s...") << file.description();
297 
298     return Con_Parse(file, (flags & CPCF_SILENT) != 0);
299 }
300 
Con_ParseCommands(NativePath const & nativePath,int flags)301 bool Con_ParseCommands(NativePath const &nativePath, int flags)
302 {
303     if (nativePath.exists())
304     {
305         std::unique_ptr<File> file(NativeFile::newStandalone(nativePath));
306         return Con_ParseCommands(*file, flags);
307     }
308     return false;
309 }
310 
Con_SetDefaultPath(Path const & path)311 void Con_SetDefaultPath(Path const &path)
312 {
313     cfgFile = path;
314 }
315 
Con_SaveDefaults()316 void Con_SaveDefaults()
317 {
318     Path path;
319 
320     if (CommandLine_CheckWith("-config", 1))
321     {
322         path = FS::accessNativeLocation(CommandLine_NextAsPath(), File::Write);
323     }
324     else
325     {
326         path = cfgFile;
327     }
328 
329     writeState(path, (!isDedicated && App_GameLoaded()?
330                              App_CurrentGame().bindingConfig() : ""));
331     Con_MarkAsChanged(false);
332 }
333 
Con_SaveDefaultsIfChanged()334 void Con_SaveDefaultsIfChanged()
335 {
336     if (DoomsdayApp::isGameLoaded() && Con_IsChanged())
337     {
338         Con_SaveDefaults();
339     }
340 }
341 
D_CMD(WriteConsole)342 D_CMD(WriteConsole)
343 {
344     DENG2_UNUSED2(src, argc);
345 
346     Path filePath(argv[1]);
347     LOG_SCR_MSG("Writing to \"%s\"...") << filePath;
348     return !writeState(filePath);
349 }
350