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