engine: reject mbf21 and shit24 wads. there is no way to know if it is safe to ignore...
[k8vavoom.git] / source / filesys / files.cpp
blob3c9c3e37e74c0ef57931959e6f6ec8ac6484320c
1 //**************************************************************************
2 //**
3 //** ## ## ## ## ## #### #### ### ###
4 //** ## ## ## ## ## ## ## ## ## ## #### ####
5 //** ## ## ## ## ## ## ## ## ## ## ## ## ## ##
6 //** ## ## ######## ## ## ## ## ## ## ## ### ##
7 //** ### ## ## ### ## ## ## ## ## ##
8 //** # ## ## # #### #### ## ##
9 //**
10 //** Copyright (C) 1999-2006 Jānis Legzdiņš
11 //** Copyright (C) 2018-2023 Ketmar Dark
12 //**
13 //** This program is free software: you can redistribute it and/or modify
14 //** it under the terms of the GNU General Public License as published by
15 //** the Free Software Foundation, version 3 of the License ONLY.
16 //**
17 //** This program is distributed in the hope that it will be useful,
18 //** but WITHOUT ANY WARRANTY; without even the implied warranty of
19 //** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 //** GNU General Public License for more details.
21 //**
22 //** You should have received a copy of the GNU General Public License
23 //** along with this program. If not, see <http://www.gnu.org/licenses/>.
24 //**
25 //**************************************************************************
26 #include "../gamedefs.h"
27 #include "../host.h"
28 #include "../../libs/core/fsys/fsys_local.h"
29 #include "../decorate/vc_decorate.h"
30 #include "files.h"
33 // from corelib, sorry!
34 extern TArray<VSearchPath *> fsysSearchPaths;
36 extern VCvarB game_release_mode;
37 //extern VCvarI game_override_mode;
38 extern int cli_NoZMapinfo; // from mapinfo.cpp
39 int cli_NoExternalDeh = 0;
41 //extern VCvarB k8gore_enabled;
42 extern VCvarI k8gore_enabled_override;
43 extern VCvarI k8gore_enabled_override_decal;
46 static VCvarB dbg_dump_gameinfo("dbg_dump_gameinfo", false, "Dump parsed game.txt?", CVAR_PreInit|CVAR_NoShadow);
47 static VCvarB gz_skip_menudef("gz_skip_menudef", false, "Skip gzdoom menudef parsing?", CVAR_PreInit|CVAR_Hidden|CVAR_NoShadow);
49 static VCvarB __dbg_debug_preinit("dbg_debug_preinit", false, "Dump preinits?", CVAR_PreInit|CVAR_Hidden|CVAR_NoShadow);
51 VCvarS game_name("game_name", "unknown", "The Name Of The Game.", CVAR_Rom|CVAR_NoShadow);
53 int cli_WAll = 0;
55 bool fsys_hasPwads = false; // or paks
56 bool fsys_hasMapPwads = false; // or paks
57 bool fsys_DisableBloodReplacement = false; // for custom modes
58 VStr flForcePlayerClass = VStr::EmptyString;
61 int fsys_warp_n0 = -1;
62 int fsys_warp_n1 = -1;
63 VStr fsys_warp_cmd = VStr::EmptyString;
65 static bool fsys_onlyOneBaseFile = false;
67 static VStr cliGameMode;
68 static const char *cliGameCStr = nullptr;
69 static TArray<VStr> autoloadDirList;
71 GameOptions game_options;
73 extern VCvarB respawnparm;
74 extern VCvarI fastparm;
75 extern VCvarB NoMonsters;
76 extern VCvarI Skill;
78 int cli_NoMonsters = -1; // not specified
79 int cli_CompileAndExit = 0;
80 static int cli_FastMonsters = -1; // not specified
81 static int cli_Respawn = -1; // not specified
82 static int cli_NoMenuDef = -1; // not specified
83 /*static*/ int cli_GoreMod = -1; // not specified
84 static int cli_GoreModForce = 0;
85 static int cli_BDWMod = -1; // not specified
86 static int cli_LudoGibsMod = -1; // not specified
87 static int cli_SkeeHUD = -1; // not specified
89 static const char *cli_BaseDir = nullptr;
90 static const char *cli_IWadName = nullptr;
91 static const char *cli_IWadDir = nullptr;
92 static const char *cli_SaveDir = nullptr;
93 static const char *cli_ConfigDir = nullptr;
95 static int reportWads = 1;
96 static int cli_NakedBase = -1;
99 struct MainWadFiles {
100 VStr main;
101 TArray<VStr> aux; // list of additinal files (if fname starts with "?", the file is optional)
102 VStr description;
106 struct GameDefinition {
107 VStr description; // game description (may be empty)
108 VStr gamename; // cannot be empty
109 TArray<MainWadFiles> mainWads; // list of wads (ony one is required)
110 TArray<VStr> params; // list of CLI params (always contains at least one item)
111 TArray<VStr> defines; // VavoomC defines to add
112 VStr GameDir; // main game dir
113 TArray<VStr> BaseDirs; // common base dirs
114 //int ParmFound;
115 bool FixVoices;
116 VStr warp; // warp template
117 TArray<VStr> filters;
118 GameOptions options;
122 VStr fl_configdir;
123 VStr fl_basedir;
124 VStr fl_savedir;
125 VStr fl_gamedir;
127 static TArray<VStr> IWadDirs;
128 static int IWadIndex;
129 static VStr warpTpl;
130 static TArray<ArgVarValue> preinitCV;
131 static ArgVarValue emptyAV;
133 static TArray<VStr> cliModesList;
134 static int cli_oldSprites = 0;
136 static TArray<GameDefinition> knownGames;
139 //==========================================================================
141 // getNetPath
143 //==========================================================================
144 static VStr getNetPath (VSearchPath *sp) {
145 if (!sp) return VStr::EmptyString;
146 if (sp->cosmetic) return VStr::EmptyString;
147 if (sp->IsNonPak()) return VStr::EmptyString;
148 VStr fname = sp->GetPrefix();
149 VStr bname = fname.extractFileName();
150 int colidx = bname.lastIndexOf(':');
151 if (colidx >= 0) bname = bname.mid(colidx+1, bname.length());
152 if (bname.isEmpty()) return VStr::EmptyString;
153 if (bname.strEquCI("basepak.pk3") || bname.strEquCI("basepak.vwad")) {
154 while (!fname.isEmpty() && fname[fname.length()-1] != '/' && fname[fname.length()-1] != '\\') fname.chopRight(1);
155 fname.chopRight(1);
156 VStr xname = fname.extractFileName();
157 if (!xname.isEmpty()) bname = xname+"/"+bname;
159 return bname;
163 // ////////////////////////////////////////////////////////////////////////// //
164 extern "C" {
165 int cliHelpSorter (const void *aa, const void *bb, void *) {
166 if (aa == bb) return 0;
167 const VParsedArgs::ArgHelp *a = (const VParsedArgs::ArgHelp *)aa;
168 const VParsedArgs::ArgHelp *b = (const VParsedArgs::ArgHelp *)bb;
169 // if any of it doesn't have help...
170 if (!!(a->arghelp) != !!(b->arghelp)) {
171 if (a->arghelp) {
172 vassert(!b->arghelp);
173 return -1;
175 if (b->arghelp) {
176 vassert(!a->arghelp);
177 return 1;
179 vassert(0);
181 return VStr::ICmp(a->argname, b->argname);
186 void FL_CollectPreinits () {
187 if (GArgs.CheckParm("-help") || GArgs.CheckParm("--help")) {
188 TArray<VParsedArgs::ArgHelp> list;
189 VParsedArgs::GetArgList(list);
190 if (list.length()) {
191 smsort_r(list.ptr(), list.length(), sizeof(VParsedArgs::ArgHelp), &cliHelpSorter, nullptr);
192 int maxlen = 0;
193 for (auto &&ainfo : list) {
194 int len = (int)strlen(ainfo.argname);
195 if (maxlen < len) maxlen = len;
197 for (auto &&ainfo : list) {
198 #ifdef _WIN32
199 fprintf(stderr, "%*s -- %s\n", -maxlen, ainfo.argname, ainfo.arghelp);
200 #else
201 GLog.Logf("%*s -- %s", -maxlen, ainfo.argname, ainfo.arghelp);
202 #endif
205 Z_Exit(0);
208 if (GArgs.CheckParm("-help-developer") || GArgs.CheckParm("--help-developer") ||
209 GArgs.CheckParm("-help-dev") || GArgs.CheckParm("--help-dev"))
211 TArray<VParsedArgs::ArgHelp> list;
212 VParsedArgs::GetArgList(list, true);
213 if (list.length()) {
214 smsort_r(list.ptr(), list.length(), sizeof(VParsedArgs::ArgHelp), &cliHelpSorter, nullptr);
215 int maxlen = 0;
216 for (auto &&ainfo : list) {
217 int len = (int)strlen(ainfo.argname);
218 if (maxlen < len) maxlen = len;
220 for (auto &&ainfo : list) {
221 #ifdef _WIN32
222 fprintf(stderr, "%*s -- %s\n", -maxlen, ainfo.argname, (ainfo.arghelp ? ainfo.arghelp : "undocumented"));
223 #else
224 GLog.Logf("%*s -- %s", -maxlen, ainfo.argname, (ainfo.arghelp ? ainfo.arghelp : "undocumented"));
225 #endif
228 Z_Exit(0);
231 int aidx = 1;
232 bool inMinus = false;
233 while (aidx < GArgs.Count()) {
234 const char *args = GArgs[aidx];
235 //GLog.Logf("FL_CollectPreinits: argc=%d; aidx=%d; inminus=%d; args=<%s>", GArgs.Count(), aidx, (int)inMinus, args);
236 if (!inMinus) {
237 if (!args[0]) {
238 GArgs.removeAt(aidx);
239 continue;
242 ++aidx;
243 // skip "-" commands
244 if (args[0] == '-') {
245 inMinus = true;
246 continue;
248 if (args[0] != '+') {
249 continue;
251 // stray "+"?
252 if (!args[1]) {
253 inMinus = true;
254 continue;
256 inMinus = false;
257 // extract command name
258 int spos = 2;
259 while (args[spos] && (vuint8)(args[spos]) > ' ') ++spos;
260 VStr vname = VStr(args+1, spos-1);
261 int flg = VCvar::GetVarFlags(*vname);
262 if (flg < 0 || !(flg&CVAR_PreInit)) {
263 inMinus = true;
264 continue;
266 // collect value
267 VStr val;
268 while (args[spos] && (vuint8)(args[spos]) <= ' ') ++spos;
269 if (args[spos]) {
270 // in the same arg
272 if (aidx < GArgs.Count()) {
273 const char *a2 = GArgs[aidx];
274 if (a2[0] != '+' && a2[0] != '-') {
275 inMinus = true;
276 continue;
280 val = VStr(args+spos);
281 // remove this arg
282 --aidx;
283 GArgs.removeAt(aidx);
284 } else {
285 // in another arg
286 if (aidx >= GArgs.Count()) break;
288 if (aidx+1 < GArgs.Count()) {
289 const char *a2 = GArgs[aidx+1];
290 if (a2[0] != '+' && a2[0] != '-') {
291 inMinus = true;
292 continue;
296 val = VStr(GArgs[aidx]);
297 // remove two args
298 --aidx;
299 GArgs.removeAt(aidx);
300 GArgs.removeAt(aidx);
302 // save it
303 ArgVarValue &vv = preinitCV.alloc();
304 vv.varname = vname;
305 vv.value = val;
306 //GLog.Logf("FL_CollectPreinits: new var '%s' with value '%s'", *vname, *val);
307 //HACK!
308 if (vname.ICmp("dbg_debug_preinit") == 0) VCvar::Set(*vname, val);
310 if (__dbg_debug_preinit && preinitCV.length()) {
311 for (int f = 0; f < GArgs.Count(); ++f) GCon->Logf("::: %d: <%s>", f, GArgs[f]);
312 for (int f = 0; f < preinitCV.length(); ++f) {
313 const ArgVarValue &vv = preinitCV[f];
314 GCon->Logf(":DOSET:%d: <%s> = <%s>", f, *vv.varname.quote(), *vv.value.quote());
320 void FL_ProcessPreInits () {
321 if (__dbg_debug_preinit) {
322 if (preinitCV.length()) for (int f = 0; f < GArgs.Count(); ++f) GCon->Logf("::: %d: <%s>", f, GArgs[f]);
324 for (int f = 0; f < preinitCV.length(); ++f) {
325 const ArgVarValue &vv = preinitCV[f];
326 VCvar::Set(*vv.varname, vv.value);
327 if (__dbg_debug_preinit) GCon->Logf(":SET:%d: <%s> = <%s>", f, *vv.varname.quote(), *vv.value.quote());
332 bool FL_HasPreInit (VStr varname) {
333 if (varname.length() == 0 || preinitCV.length() == 0) return false;
334 for (int f = 0; f < preinitCV.length(); ++f) {
335 if (preinitCV[f].varname.ICmp(varname) == 0) return true;
337 return false;
341 void FL_ClearPreInits () {
342 preinitCV.clear();
346 // used to set "preinit" cvars
347 int FL_GetPreInitCount () {
348 return preinitCV.length();
352 const ArgVarValue &FL_GetPreInitAt (int idx) {
353 if (idx < 0 || idx >= preinitCV.length()) return emptyAV;
354 return preinitCV[idx];
358 // ////////////////////////////////////////////////////////////////////////// //
359 struct GroupMask {
360 VStr mask;
361 bool enabled;
363 bool isGlob () const { return (mask.indexOf('*') >= 0 || mask.indexOf('?') >= 0 || mask.indexOf('[') >= 0); }
364 bool match (VStr s) const { return s.globmatch(mask, false); }
368 struct GroupPwadInfo {
369 VStr filename; // with path, maybe
370 bool cosmetic; // is this pwad a cosmetic one (i.e. doesn't affect saves?)
374 struct CustomModeInfo {
375 VStr name;
376 TArray<VStr> aliases;
377 TArray<GroupPwadInfo> pwads;
378 TArray<GroupPwadInfo> postpwads;
379 TArray<GroupMask> autoskips;
380 bool disableBloodReplacement;
381 bool disableGoreMod;
382 bool disableBDW;
383 VStr ForcePlayerClass;
384 VStr basedir;
385 // has any sense only for modes loaded from "~/.k8vavoom/modes.rc"
386 TArray<VStr> basedirglob;
387 bool reported;
389 CustomModeInfo () : name(), aliases(), pwads(), postpwads(), autoskips(), disableBloodReplacement(false), disableGoreMod(false), disableBDW(false), ForcePlayerClass(), basedir(), basedirglob(), reported(false) {}
391 CustomModeInfo (const CustomModeInfo &src) : name(), aliases(), pwads(), postpwads(), autoskips(), disableBloodReplacement(false), disableGoreMod(false), disableBDW(false), ForcePlayerClass(), basedir(), basedirglob(), reported(false) { copyFrom(src); }
393 void operator = (const CustomModeInfo &src) { copyFrom(src); }
395 void clear () {
396 name.clear();
397 aliases.clear();
398 pwads.clear();
399 postpwads.clear();
400 autoskips.clear();
401 disableBloodReplacement = false;
402 disableGoreMod = false;
403 disableBDW = false;
404 ForcePlayerClass.clear();
405 basedirglob.clear();
406 reported = false;
409 void appendPWad (VStr str, bool asCosmetic) {
410 if (str.isEmpty()) return;
411 for (auto &&s : pwads) if (s.filename.strEqu(str)) return;
412 GroupPwadInfo &pi = pwads.alloc();
413 pi.filename = str;
414 pi.cosmetic = asCosmetic;
417 void appendPostPWad (VStr str, bool asCosmetic) {
418 if (str.isEmpty()) return;
419 for (auto &&s : postpwads) if (s.filename.strEqu(str)) return;
420 GroupPwadInfo &pi = postpwads.alloc();
421 pi.filename = str;
422 pi.cosmetic = asCosmetic;
425 void copyFrom (const CustomModeInfo &src) {
426 if (&src == this) return;
427 clear();
428 name = src.name;
429 // copy aliases
430 aliases.setLength(src.aliases.length());
431 for (auto &&it : src.aliases.itemsIdx()) aliases[it.index()] = it.value();
432 // copy pwads
433 pwads.setLength(src.pwads.length());
434 for (auto &&it : src.pwads.itemsIdx()) pwads[it.index()] = it.value();
435 // copy postwads
436 postpwads.setLength(src.postpwads.length());
437 for (auto &&it : src.postpwads.itemsIdx()) postpwads[it.index()] = it.value();
438 // copy autoskips
439 autoskips.setLength(src.autoskips.length());
440 for (auto &&it : src.autoskips.itemsIdx()) autoskips[it.index()] = it.value();
441 // copy flags
442 disableBloodReplacement = src.disableBloodReplacement;
443 disableGoreMod = src.disableGoreMod;
444 disableBDW = src.disableBDW;
445 // copy other things
446 ForcePlayerClass = src.ForcePlayerClass;
447 basedir = src.basedir;
448 basedirglob.setLength(src.basedirglob.length());
449 for (auto &&it : src.basedirglob.itemsIdx()) basedirglob[it.index()] = it.value();
450 reported = src.reported;
453 // used to build final mode definition
454 void merge (const CustomModeInfo &src) {
455 if (&src == this) return;
456 for (auto &&s : src.pwads) appendPWad(s.filename, s.cosmetic);
457 for (auto &&s : src.postpwads) appendPostPWad(s.filename, s.cosmetic);
458 for (auto &&s : src.autoskips) autoskips.append(s);
459 if (src.disableBloodReplacement) disableBloodReplacement = true;
460 if (src.disableGoreMod) disableGoreMod = true;
461 if (src.disableBDW) disableBDW = true;
462 if (!src.ForcePlayerClass.isEmpty()) ForcePlayerClass = src.ForcePlayerClass;
465 void dump () const {
466 GLog.Logf(NAME_Debug, "==== MODE: %s ====", *name);
467 GLog.Logf(NAME_Debug, "basedir: <%s>", *basedir);
468 GLog.Logf(NAME_Debug, "basedirglobs: %d", basedirglob.length());
469 for (auto &&s : basedirglob) GLog.Logf(NAME_Debug, " <%s>", *s);
470 GLog.Logf(NAME_Debug, "pwads: %d", pwads.length());
471 for (auto &&s : pwads) GLog.Logf(NAME_Debug, " <%s>%s", *s.filename, (s.cosmetic ? " (cosmetic)" : ""));
472 GLog.Logf(NAME_Debug, "postpwads: %d", postpwads.length());
473 for (auto &&s : postpwads) GLog.Logf(NAME_Debug, " <%s>%s", *s.filename, (s.cosmetic ? " (cosmetic)" : ""));
474 GLog.Logf(NAME_Debug, "autoskips: %d", autoskips.length());
475 for (auto &&s : autoskips) GLog.Logf(NAME_Debug, " <%s> (%d)", *s.mask, (int)s.enabled);
476 GLog.Logf(NAME_Debug, "disableBloodReplacement: %s", (disableBloodReplacement ? "tan" : "ona"));
477 GLog.Logf(NAME_Debug, "disableGoreMod: %s", (disableGoreMod ? "tan" : "ona"));
478 GLog.Logf(NAME_Debug, "disableBDW: %s", (disableBDW ? "tan" : "ona"));
479 GLog.Log(NAME_Debug, "========");
482 bool isMyAlias (VStr s) const {
483 s = s.xstrip();
484 if (s.isEmpty()) return false;
485 for (auto &&a : aliases) if (a.strEquCI(s)) return true;
486 return false;
489 static VStr stripBaseDirShit (VStr dirname) {
490 while (!dirname.isEmpty() && (dirname[0] == '/' || dirname[0] == '\\')) dirname.chopLeft(1);
491 while (!dirname.isEmpty() && (dirname[dirname.length()-1] == '/' || dirname[dirname.length()-1] == '\\')) dirname.chopRight(1);
492 if (dirname.startsWithCI("basev/")) dirname.chopLeft(6);
493 return dirname;
496 bool isGoodBaseDir (VStr dirname) const {
497 if (basedirglob.length() == 0) return true;
498 dirname = stripBaseDirShit(dirname);
499 if (dirname.isEmpty()) return true;
500 for (auto &&g : basedirglob) if (dirname.globMatchCI(g)) return true;
501 return false;
504 // returns "cosmetic" flag
505 static bool processPWadFlags (VStr &path) {
506 path = path.xstrip();
507 if (path.isEmpty()) return false;
508 if (path[0] == '!') {
509 path.chopLeft(1);
510 path = path.xstrip();
511 return false;
513 if (path[0] == '*') {
514 path.chopLeft(1);
515 path = path.xstrip();
516 return true;
518 return false;
521 // `mode` keyword skipped, expects mode name
522 // mode must be cleared
523 void parse (VScriptParser *sc) {
524 //clear();
525 sc->ExpectString();
526 name = sc->String;
527 sc->Expect("{");
528 while (!sc->Check("}")) {
529 if (sc->Check("pwad")) {
530 for (;;) {
531 sc->ExpectString();
532 const bool cosmetic = processPWadFlags(sc->String);
533 if (sc->String.isEmpty()) continue;
534 appendPWad(sc->String, cosmetic);
535 if (!sc->Check(",")) break;
537 } else if (sc->Check("postpwad")) {
538 for (;;) {
539 sc->ExpectString();
540 const bool cosmetic = processPWadFlags(sc->String);
541 if (sc->String.isEmpty()) continue;
542 appendPostPWad(sc->String, cosmetic);
543 if (!sc->Check(",")) break;
545 } else if (sc->Check("skipauto")) {
546 for (;;) {
547 sc->ExpectString();
548 sc->String = sc->String.xstrip();
549 if (!sc->String.isEmpty()) {
550 GroupMask &gi = autoskips.alloc();
551 gi.mask = sc->String;
552 gi.enabled = false;
554 if (!sc->Check(",")) break;
556 } else if (sc->Check("forceauto")) {
557 for (;;) {
558 sc->ExpectString();
559 GroupMask &gi = autoskips.alloc();
560 gi.mask = sc->String;
561 gi.enabled = true;
562 if (!sc->Check(",")) break;
564 } else if (sc->Check("DisableBloodReplacement")) {
565 disableBloodReplacement = true;
566 } else if (sc->Check("DisableGoreMod")) {
567 disableGoreMod = true;
568 } else if (sc->Check("DisableBDW")) {
569 disableBDW = true;
570 } else if (sc->Check("ForcePlayerClass")) {
571 sc->ExpectString();
572 ForcePlayerClass = sc->String.xstrip();
573 } else if (sc->Check("alias")) {
574 sc->ExpectString();
575 VStr k = sc->String.xstrip();
576 if (!k.isEmpty()) aliases.append(k);
577 } else if (sc->Check("basedir")) {
578 sc->ExpectString();
579 VStr k = stripBaseDirShit(sc->String.xstrip());
580 if (!k.isEmpty()) basedirglob.append(k);
581 } else {
582 sc->Error(va("unknown command '%s'", *sc->String));
589 static CustomModeInfo customMode;
590 static TArray<CustomModeInfo> userModes; // from "~/.k8vavoom/modes.rc"
591 static TArray<GroupPwadInfo> postPWads; // from autoload
592 static bool modDetectorDisabledIWads = false;
593 static bool modNoBaseSprOfs = false;
594 static TArray<VStr> modAddMods;
597 //==========================================================================
599 // mdetect_AddMod
601 //==========================================================================
602 VVA_OKUNUSED static void mdetect_AddMod (VStr s) {
603 if (s.isEmpty()) return;
604 for (auto &&mm : modAddMods) if (mm.strEquCI(s)) return;
605 modAddMods.append(s);
609 //==========================================================================
611 // mdetect_ClearAndBlockCustomModes
613 //==========================================================================
614 VVA_OKUNUSED static void mdetect_ClearAndBlockCustomModes () {
615 customMode.clear();
616 userModes.clear();
617 postPWads.clear();
618 cli_NakedBase = 1; // ignore autoloads
619 fsys_onlyOneBaseFile = true;
623 //==========================================================================
625 // mdetect_DisableBDW
627 //==========================================================================
628 VVA_OKUNUSED static void mdetect_DisableBDW () {
629 if (!fsys_DisableBDW) GCon->Logf(NAME_Init, "BDW mod disabled.");
630 fsys_DisableBDW = true;
634 //==========================================================================
636 // mdetect_DisableGore
638 //==========================================================================
639 VVA_OKUNUSED static void mdetect_DisableGore () {
640 if (cli_GoreMod != 0) GCon->Logf(NAME_Init, "Gore mod disabled.");
641 cli_GoreMod = 0;
645 //==========================================================================
647 // mdetect_DisableIWads
649 //==========================================================================
650 VVA_OKUNUSED static void mdetect_DisableIWads () {
651 if (!modDetectorDisabledIWads) GCon->Logf(NAME_Init, "IWAD disabled.");
652 modDetectorDisabledIWads = true;
656 //==========================================================================
658 // mdetect_SetGameName
660 //==========================================================================
661 VVA_OKUNUSED static void mdetect_SetGameName (VStr gname) {
662 cliGameMode = gname;
666 #include "fsmoddetect.cpp"
669 //==========================================================================
671 // LoadModesFromStream
673 // deletes stream
675 //==========================================================================
676 static void LoadModesFromStream (VStream *rcstrm, TArray<CustomModeInfo> &modes, VStr basedir=VStr::EmptyString) {
677 if (!rcstrm) return;
678 VScriptParser *sc = new VScriptParser(rcstrm->GetName(), rcstrm);
679 while (!sc->AtEnd()) {
680 if (sc->Check("alias")) {
681 sc->ExpectString();
682 VStr k = sc->String.xstrip();
683 sc->Expect("is");
684 sc->ExpectString();
685 VStr v = sc->String.xstrip();
686 if (!k.isEmpty() && !v.isEmpty() && !k.strEquCI(v)) {
687 bool found = false;
688 for (int f = 0; f < modes.length(); ++f) {
689 if (modes[f].name.strEquCI(v)) {
690 found = true;
691 modes[f].aliases.append(k);
692 break;
695 if (!found) sc->Message(va("cannot set alias '%s', because mode '%s' is unknown", *k, *v));
697 continue;
699 sc->Expect("mode");
700 CustomModeInfo mode;
701 mode.clear();
702 mode.basedir = basedir;
703 mode.parse(sc);
704 // append mode
705 bool found = false;
706 for (int f = 0; f < modes.length(); ++f) {
707 if (modes[f].name.strEquCI(mode.name)) {
708 // i found her!
709 found = true;
710 modes[f] = mode;
711 break;
714 if (!found) modes.append(mode);
716 delete sc;
720 //==========================================================================
722 // ParseUserModes
724 // "game_name" cvar must be set
726 //==========================================================================
727 static void ParseUserModes () {
728 if (game_name.asStr().isEmpty()) return; // just in case
729 VStream *rcstrm = FL_OpenFileReadInCfgDir("modes.rc");
730 if (!rcstrm) return;
731 GCon->Logf(NAME_Init, "parsing user mode definitions from '%s'", *rcstrm->GetName());
733 // load modes
734 LoadModesFromStream(rcstrm, userModes);
738 //==========================================================================
740 // ApplyUserModes
742 //==========================================================================
743 static void ApplyUserModes (VStr basedir) {
744 // apply modes
745 for (auto &&mname : cliModesList) {
746 CustomModeInfo *nfo = nullptr;
747 for (auto &&mode : userModes) {
748 if (mode.isGoodBaseDir(basedir) && mode.name.strEquCI(mname)) {
749 nfo = &mode;
750 break;
753 if (!nfo) {
754 for (auto &&mode : userModes) {
755 if (mode.isGoodBaseDir(basedir) && mode.isMyAlias(mname)) {
756 nfo = &mode;
757 break;
761 if (nfo) {
762 if (!nfo->reported) {
763 nfo->reported = true;
764 GCon->Logf(NAME_Init, "activating user mode '%s'", *nfo->name);
766 customMode.merge(*nfo);
772 //==========================================================================
774 // SetupCustomMode
776 //==========================================================================
777 static void SetupCustomMode (VStr basedir) {
778 //customMode.clear();
780 if (!basedir.isEmpty() && !basedir.endsWith("/")) basedir += "/";
781 VStream *rcstrm = FL_OpenSysFileRead(basedir+"modes.rc");
782 if (!rcstrm) return;
783 GCon->Logf(NAME_Init, "parsing mode definitions from '%s'", *rcstrm->GetName());
785 // load modes
786 TArray<CustomModeInfo> modes;
787 LoadModesFromStream(rcstrm, modes, basedir);
788 if (modes.length() == 0) return; // nothing to do
790 // build active mode
791 customMode.basedir = basedir;
792 for (auto &&mname : cliModesList) {
793 const CustomModeInfo *nfo = nullptr;
794 for (auto &&mode : modes) {
795 if (mode.name.strEquCI(mname)) {
796 nfo = &mode;
797 break;
800 if (!nfo) {
801 for (auto &&mode : modes) {
802 if (mode.isMyAlias(mname)) {
803 nfo = &mode;
804 break;
808 if (nfo) {
809 GCon->Logf(NAME_Init, "activating mode '%s'", *nfo->name);
810 customMode.merge(*nfo);
816 // ////////////////////////////////////////////////////////////////////////// //
817 struct PWadFile {
818 VStr fname;
819 bool skipSounds;
820 bool skipSprites;
821 bool skipDehacked;
822 bool storeInSave;
823 bool asDirectory;
825 PWadFile ()
826 : fname()
827 , skipSounds(false)
828 , skipSprites(false)
829 , skipDehacked(false)
830 , storeInSave(false)
831 , asDirectory(false)
836 static TArray<PWadFile> pwadList;
837 static int doStartMap = 0;
840 static int pwflag_SkipSounds = 0;
841 static int pwflag_SkipSprites = 0;
842 static int pwflag_SkipDehacked = 0;
843 static int pwflag_SkipSaveList = 0;
846 //**************************************************************************
848 // pwad scanner (used to pre-scan pwads to extract various things)
850 //**************************************************************************
852 struct PWadScanInfo {
853 bool processed;
854 VStr iwad; // guessed from gameinfo; no path
855 VStr mapname; // name of the arbitrary map from pwads
856 VStr maplump; // ...for the above
857 int episode; // 0: doom2
858 int mapnum;
859 bool hasMapinfo;
861 PWadScanInfo () noexcept : processed(false), iwad(), mapname(), maplump(), episode(-1), mapnum(-1), hasMapinfo(false) {}
862 inline void clear () noexcept { processed = false; iwad.clear(); mapname.clear(); maplump.clear(); episode = -1; mapnum = -1; hasMapinfo = false; }
864 inline bool isMapIndexValid () const noexcept { return (episode >= 0 && mapnum >= 0); }
866 inline int getMapIndex () const noexcept { return (episode > 0 ? episode*10+mapnum : episode == 0 ? mapnum : 0); }
868 static inline int exmxToIndex (int e, int m) noexcept { return (e*10+m); }
872 static PWadScanInfo pwadScanInfo;
873 TArray<PWadMapLump> fsys_PWadMaps; // sorted
875 extern "C" {
876 static int cmpPWadMapLump (const void *aa, const void *bb, void *) {
877 if (aa == bb) return 0;
878 const PWadMapLump *a = (const PWadMapLump *)aa;
879 const PWadMapLump *b = (const PWadMapLump *)bb;
880 if (a->episode && b->episode) return a->getMapIndex()-b->getMapIndex();
881 if (!a->episode && !b->episode) return a->getMapIndex()-b->getMapIndex();
882 if (a->episode) return -1; // b is D2 map
883 if (b->episode) return 1; // a is D2 map
884 // just in case
885 return a->getMapIndex()-b->getMapIndex();
890 //==========================================================================
892 // appendPWadMapLump
894 //==========================================================================
895 static void appendPWadMapLump (const PWadMapLump &wlmp) {
896 if (!wlmp.isValid()) return;
897 for (auto &&l : fsys_PWadMaps) {
898 if (l.isEqual(wlmp)) {
899 l = wlmp;
900 return;
903 fsys_PWadMaps.append(wlmp);
907 //==========================================================================
909 // PWadMapLump::parseMapName
911 //==========================================================================
912 bool PWadMapLump::parseMapName (const char *name) noexcept {
913 clear();
914 if (!name || !name[0]) return false;
916 // doom1, kdizd
917 if ((name[0] == 'e' || name[0] == 'z') && name[1] && name[2] == 'm' && name[3] && !name[4]) {
918 int e = VStr::digitInBase(name[1], 10);
919 int m = VStr::digitInBase(name[3], 10);
920 if (e < 0 || m < 0) return false;
921 if (e >= 1 && e <= 9 && m >= 1 && m <= 9) {
922 mapname = VStr(name);
923 episode = e;
924 mapnum = m;
925 return true;
929 // doom2
930 int n = -1;
931 // try to detect things like "aaa<digit>"
932 int dpos = 0;
933 while (name[dpos] && VStr::digitInBase(name[dpos], 10) < 0) ++dpos;
934 if (dpos == 0) return false; // must start from a lettter
935 if (VStr::digitInBase(name[dpos], 10) < 0) return false; // nope
936 for (const char *t = name+dpos; *t; ++t) if (VStr::digitInBase(*t, 10) < 0) return false;
937 if (!VStr::convertInt(name+dpos, &n)) return false;
938 if (n < 1 || n > 99) return false;
939 // i found her!
940 mapname = VStr(name);
941 episode = 0;
942 mapnum = n;
943 return true;
947 //==========================================================================
949 // processMapName
951 //==========================================================================
952 static void processMapName (const char *name, int lump) {
953 if (!name || !name[0]) return;
954 //GCon->Logf(NAME_Debug, "*** checking map: name=<%s>", name);
956 //TODO: use `PWadMapLump::parseMapName()` here, and remove pasta
957 PWadMapLump wlmp;
958 if (wlmp.parseMapName(name)) {
959 wlmp.lump = lump;
960 appendPWadMapLump(wlmp);
963 // if we have maps for both D1 and D2 (Maps Of Chaos, for example), use D2 maps
965 // doom1 (or kdizd)
966 if (pwadScanInfo.episode != 0) {
967 if ((name[0] == 'e' || name[0] == 'z') && name[1] && name[2] == 'm' && name[3] && !name[4]) {
968 int e = VStr::digitInBase(name[1], 10);
969 int m = VStr::digitInBase(name[3], 10);
970 if (e < 0 || m < 0) return;
971 if (e >= 1 && e <= 9 && m >= 1 && m <= 9) {
972 //if (pwadScanInfo.episode == 0) return; // oops, mixed maps
973 const int newidx = PWadScanInfo::exmxToIndex(e, m);
974 if (pwadScanInfo.episode < 0 || pwadScanInfo.getMapIndex() > newidx) {
975 pwadScanInfo.episode = e;
976 pwadScanInfo.mapnum = m;
977 pwadScanInfo.mapname = VStr(name);
978 if (pwadScanInfo.maplump.isEmpty()) pwadScanInfo.maplump = W_FullLumpName(lump);
979 //GCon->Logf(NAME_Debug, "*** D1 MAP; episode=%d; map=%d; index=%d", e, m, pwadScanInfo.getMapIndex());
982 return; // continue
986 // doom2
987 /*if (pwadScanInfo.episode <= 0)*/ {
988 int n = -1;
989 // try to detect things like "aaa<digit>"
990 int dpos = 0;
991 while (name[dpos] && VStr::digitInBase(name[dpos], 10) < 0) ++dpos;
992 if (dpos == 0) return; // must start from a lettter
993 if (VStr::digitInBase(name[dpos], 10) < 0) return; // nope
994 for (const char *t = name+dpos; *t; ++t) if (VStr::digitInBase(*t, 10) < 0) return;
995 if (!VStr::convertInt(name+dpos, &n)) return;
996 if (n < 1) return;
997 //GCon->Logf(NAME_Debug, " n=%d", n);
998 // if D1 map were found, perform some hackery
999 // only "MAPxx" is allowed to override it
1000 if (pwadScanInfo.episode > 0) {
1001 VStr pfx(name);
1002 pfx = pfx.left(dpos);
1003 //GCon->Logf(NAME_Debug, " pfx=<%s>; len=%d", *pfx, VStr::Length(name));
1004 if (!pfx.startsWithCI("MAP") || VStr::Length(name) != 5) return;
1005 pwadScanInfo.clear();
1007 pwadScanInfo.episode = 0;
1008 if (pwadScanInfo.mapnum < 0 || pwadScanInfo.mapnum > n) {
1009 pwadScanInfo.mapnum = n;
1010 pwadScanInfo.mapname = VStr(name);
1011 if (pwadScanInfo.maplump.isEmpty()) pwadScanInfo.maplump = W_FullLumpName(lump);
1012 //GCon->Logf(NAME_Debug, "*** D2 MAP; index=%d", pwadScanInfo.getMapIndex());
1014 return; // continue
1019 //==========================================================================
1021 // findMapChecker
1023 //==========================================================================
1024 static void findMapChecker (int lump) {
1025 VName lumpname = W_LumpName(lump);
1026 if (lumpname == NAME_None) return;
1027 processMapName(*lumpname, lump);
1031 //==========================================================================
1033 // performPWadScan
1035 // it is safe to call this several times
1036 // results will be put into `pwadScanInfo`
1038 //==========================================================================
1039 static void performPWadScan () {
1040 if (pwadScanInfo.processed) return;
1042 pwadScanInfo.clear();
1043 pwadScanInfo.processed = true;
1045 VStr giwad;
1047 auto milump = (cli_NoZMapinfo >= 0 ? W_CheckNumForName("zmapinfo") : -1);
1048 if (milump < 0 /*!!! || !W_IsAuxLump(milump)*/) milump = W_CheckNumForName("mapinfo");
1049 pwadScanInfo.hasMapinfo = (milump >= 0 /*!!! && W_IsAuxLump(milump)*/);
1051 auto xilump = W_CheckNumForName("complvl");
1052 if (xilump >= 0) {
1053 VScriptParser *gsc = VScriptParser::NewWithLump(xilump);
1054 gsc->SetCMode(true);
1055 gsc->GetString();
1056 if (gsc->String.strEquCI("mbf21")) {
1057 Sys_Error("COMPLVL with \"mbf21\" found. k8vavoom doesn't support MBF21.");
1059 delete gsc;
1062 //GCon->Log("**********************************");
1063 // try "GAMEINFO" first
1064 auto gilump = W_CheckNumForName("gameinfo");
1065 if (gilump >= 0/*!!! W_IsAuxLump(gilump)*/) {
1066 VScriptParser *gsc = VScriptParser::NewWithLump(gilump);
1067 gsc->SetCMode(true);
1068 while (gsc->GetString()) {
1069 if (gsc->QuotedString) continue; // just in case
1070 if (gsc->String.strEquCI("iwad")) {
1071 gsc->Check("=");
1072 if (gsc->GetString()) pwadScanInfo.iwad = gsc->String;
1073 continue;
1076 delete gsc;
1077 pwadScanInfo.iwad = pwadScanInfo.iwad.ExtractFileBaseName();
1078 if (!pwadScanInfo.iwad.isEmpty()) {
1079 if (!pwadScanInfo.iwad.extractFileExtension().strEquCI(".wad")) pwadScanInfo.iwad += ".wad";
1080 GCon->Logf(NAME_Init, "gameinfo iwad: '%s'", *pwadScanInfo.iwad);
1081 giwad = pwadScanInfo.iwad;
1085 // guess the name of the first map
1086 GCon->Log(NAME_Init, "detecting pwad maps");
1087 for (auto &&it : WadMapIterator::FromWadFile(0)/*!!! FromFirstAuxFile()*/) findMapChecker(it.lump);
1088 // if no ordinary maps, try to find "maps/xxx.wad" files
1089 if (!pwadScanInfo.isMapIndexValid()) {
1090 //for (auto &&it : WadFileIterator::FromFirstAuxFile()) {
1091 for (auto &&it : WadNSIterator::FromWadFile(0/*!!! W_GetFirstAuxFile()*/, WADNS_AllFiles)) {
1092 VStr fname = it.getRealName();
1093 // check for "maps/xxx.wad"
1094 //GCon->Logf(NAME_Debug, ":: <%s>", *fname);
1095 if (fname.startsWithCI("maps/") &&
1096 (fname.endsWithCI(".wad") || fname.endsWithCI(".vwad")))
1098 bool fucked = false;
1099 const char *s = fname.getCStr() + 5;
1100 while (!fucked && *s) { fucked = (*s == '/'); s += 1; }
1101 if (!fucked) {
1102 fname.chopLeft(5);
1103 if (fname.endsWithCI(".wad")) fname.chopRight(4); else fname.chopRight(5);
1104 processMapName(*fname, it.lump);
1109 // sort collected maps
1110 smsort_r(fsys_PWadMaps.ptr(), fsys_PWadMaps.length(), sizeof(PWadMapLump), &cmpPWadMapLump, nullptr);
1111 GCon->Log(NAME_Init, "pwad map detection complete.");
1113 fsys_hasMapPwads = (fsys_PWadMaps.length() > 0);
1114 pwadScanInfo.iwad = giwad;
1118 // ////////////////////////////////////////////////////////////////////////// //
1119 static TArray<VStr> wpklist; // everything is lowercased
1120 static TArray<VStr> wpklistSmall; // everything is lowercased
1123 //==========================================================================
1125 // fixWpkFileName
1127 //==========================================================================
1128 static VStr fixWpkFileName (VStr fname) {
1129 if (fname.IsEmpty()) return VStr::EmptyString;
1130 // yeah, assume that they are the same
1131 // (because i will prolly convert them)
1132 if (fname == "player_zan.vwad") {
1133 fname.chopRight(5);
1134 fname += ".pk3";
1136 return fname;
1140 //==========================================================================
1142 // appendWpkSmallName
1144 //==========================================================================
1145 static void appendWpkSmallName (VStr fname) {
1146 if (fname.IsEmpty()) return;
1147 fname = fname.toLowerCase();
1148 // yeah, assume that they are the same
1149 // (because i will prolly convert them)
1150 if (fname.endsWith(".pk3") || fname.endsWith(".zip") ||
1151 fname.endsWith(".ipk3"))
1153 fname.chopRight(4);
1154 fname += ".vwad";
1156 for (VStr ws : wpklistSmall) if (ws == fname) return; // i found her!
1157 wpklistSmall.Append(fname);
1161 //==========================================================================
1163 // FL_GetWadPk3List
1165 //==========================================================================
1166 const TArray<VStr> &FL_GetWadPk3List () {
1167 return wpklist;
1171 //==========================================================================
1173 // FL_GetWadPk3ListSmall
1175 //==========================================================================
1176 const TArray<VStr> &FL_GetWadPk3ListSmall () {
1177 return wpklistSmall;
1181 //==========================================================================
1183 // wpkAppend
1185 //==========================================================================
1186 static void wpkAppend (VStr fname, bool asystem) {
1187 if (fname.length() == 0) return;
1188 VStr fn = fname.toLowerCase();
1189 if (!asystem) {
1190 fn = fn.extractFileName();
1191 if (fn.length() == 0) fn = fname.toLowerCase();
1193 //if (fname.endsWithCI(".zip")) return; // ignore zip containers
1194 //GCon->Logf(NAME_Debug, "WPK: %s", *fn);
1195 appendWpkSmallName(fn);
1196 // check for duplicates
1197 for (VStr ws : wpklist) if (ws == fn) return; // i found her!
1198 wpklist.append(fn);
1202 //==========================================================================
1204 // wpkMark
1206 // call this before adding archives
1208 //==========================================================================
1209 static int wpkMark (bool cosmetic) {
1210 return (cosmetic ? -1 : fsysSearchPaths.length());
1214 //==========================================================================
1216 // wpkAddMarked
1218 //==========================================================================
1219 static void wpkAddMarked (int idx) {
1220 if (idx < 0) return;
1221 for (; idx < fsysSearchPaths.length(); ++idx) {
1222 VSearchPath *sp = fsysSearchPaths[idx];
1224 if (sp->cosmetic) continue; // just in case
1225 if (sp->IsNonPak()) continue;
1226 VStr fn = sp->GetPrefix().extractFileName().toLowerCase();
1228 VStr fn = getNetPath(sp);
1229 if (fn.isEmpty()) continue;
1230 //GCon->Logf(NAME_Debug, "*** <%s> (%s) ***", *fn, *sp->GetPrefix());
1231 wpklist.append(fixWpkFileName(fn));
1232 // simplified list
1233 // lone files marked as paks too
1234 if (!sp->IsLoneWadArchive() && sp->IsAnyPak()) {
1235 appendWpkSmallName(fn);
1241 // ////////////////////////////////////////////////////////////////////////// //
1243 VVA_OKUNUSED static int cmpfuncCI (const void *v1, const void *v2) {
1244 return ((VStr *)v1)->ICmp((*(VStr *)v2));
1248 VVA_OKUNUSED static int cmpfuncCINoExt (const void *v1, const void *v2, void *) {
1249 return ((VStr *)v1)->StripExtension().ICmp(((VStr *)v2)->StripExtension());
1253 //==========================================================================
1255 // AddAnyFile
1257 //==========================================================================
1258 static bool AddAnyFile (VStr fname, bool allowFail, bool fixVoices=false) {
1259 if (fname.length() == 0) {
1260 if (!allowFail) Sys_Error("cannot add empty file");
1261 return false;
1263 if (!Sys_FileExists(fname)) {
1264 if (!allowFail) Sys_Error("cannot add file \"%s\"", *fname);
1265 GCon->Logf(NAME_Warning, "cannot add file \"%s\"", *fname);
1266 return false;
1268 if (!W_AddDiskFileOptional(fname, (allowFail ? false : fixVoices))) {
1269 if (!allowFail) Sys_Error("cannot add file \"%s\"", *fname);
1270 GCon->Logf(NAME_Warning, "cannot add file \"%s\"", *fname);
1271 return false;
1273 return true;
1277 //==========================================================================
1279 // AddAnyUserFile
1281 //==========================================================================
1282 static void AddAnyUserFile (VStr fname, bool cosmetic) {
1283 auto mark = wpkMark(cosmetic);
1284 if (!AddAnyFile(fname, true/*allowFail*/)) return;
1285 //if (!cosmetic) wpkAppend(fname, false);
1286 wpkAddMarked(mark);
1290 // ////////////////////////////////////////////////////////////////////////// //
1291 enum { CM_PRE_PWADS, CM_POST_PWADS };
1293 //==========================================================================
1295 // CustomModeLoadPwads
1297 //==========================================================================
1298 static void CustomModeLoadPwads (int type) {
1299 TArray<GroupPwadInfo> &list = (type == CM_PRE_PWADS ? customMode.pwads : customMode.postpwads);
1300 //GCon->Logf(NAME_Init, "CustomModeLoadPwads: type=%d; len=%d", type, list.length());
1302 // load post-file pwads from autoload here too
1303 if (type == CM_POST_PWADS && postPWads.length() > 0) {
1304 GCon->Logf(NAME_Init, "loading autoload post-pwads");
1305 for (auto &&wn : postPWads) {
1306 GCon->Logf(NAME_Init, "autoload %spost-pwad: %s", (wn.cosmetic ? "cosmetic " : ""), *wn.filename);
1307 AddAnyUserFile(wn.filename, wn.cosmetic); // allow fail
1311 for (auto &&ww : list) {
1312 if (ww.filename.isEmpty()) continue;
1313 VStr fname = ww.filename;
1314 if (!fname.startsWith("/")) fname = customMode.basedir+fname;
1315 GCon->Logf(NAME_Init, "%smode pwad: %s", (ww.cosmetic ? "cosmetic " : ""), *fname);
1316 AddAnyUserFile(fname, ww.cosmetic); // allow fail
1321 static TMap<VStr, bool> cliGroupMap; // true: enabled; false: disabled
1322 static TArray<GroupMask> cliGroupMask;
1325 //==========================================================================
1327 // AddAutoloadRC
1329 //==========================================================================
1330 void AddAutoloadRC (VStr aubasedir) {
1331 VStream *aurc = FL_OpenSysFileRead(aubasedir+"autoload.rc");
1332 if (!aurc) return;
1334 // collect autoload groups to skip
1335 // add skips from custom mode
1336 for (int f = 0; f < customMode.autoskips.length(); ++f) {
1337 GroupMask &gi = customMode.autoskips[f];
1338 //GCon->Logf(NAME_Debug, "f=%d; %s=<%s>; enabled=%s", f, (gi.isGlob() ? "glob" : "name"), *gi.mask, (gi.enabled ? "tan" : "ona"));
1339 if (gi.isGlob()) {
1340 cliGroupMask.append(gi);
1341 } else {
1342 cliGroupMap.put(gi.mask.toLowerCase(), gi.enabled);
1346 GCon->Logf(NAME_Init, "parsing autoload groups from '%s'", *aurc->GetName());
1347 VScriptParser *sc = new VScriptParser(aurc->GetName(), aurc);
1349 while (!sc->AtEnd()) {
1350 sc->Expect("group");
1351 sc->ExpectString();
1352 VStr grpname = sc->String;
1353 bool enabled = !sc->Check("disabled");
1354 sc->Expect("{");
1355 // exact matches has precedence
1356 //GCon->Logf(" GROUP: <%s>", *grpname);
1357 auto gmp = cliGroupMap.get(grpname.toLowerCase());
1358 if (gmp) {
1359 enabled = *gmp;
1360 } else {
1361 // process masks; backwards, so latest mask has precedence
1362 for (int f = cliGroupMask.length()-1; f >= 0; --f) {
1363 if (grpname.globmatch(cliGroupMask[f].mask, false)) {
1364 enabled = cliGroupMask[f].enabled;
1365 break;
1369 if (!enabled) {
1370 GCon->Logf(NAME_Init, "skipping autoload group '%s'", *grpname);
1371 sc->SkipBracketed(true); // bracket eaten
1372 continue;
1374 GCon->Logf(NAME_Init, "processing autoload group '%s'", *grpname);
1375 // get file list
1376 while (!sc->Check("}")) {
1377 if (sc->Check(",")) continue;
1378 bool postPWad = false;
1379 bool cosmetic = true;
1380 if (sc->Check("postpwad")) postPWad = true;
1381 if (!sc->GetString()) sc->Error("wad/pk3 path expected");
1382 if (sc->String.isEmpty()) continue;
1383 if (sc->String[0] == '!') {
1384 sc->String.chopLeft(1);
1385 if (sc->String.isEmpty()) continue;
1386 cosmetic = false;
1388 VStr fname = ((*sc->String)[0] == '/' ? sc->String : aubasedir+sc->String);
1389 if (postPWad) {
1390 GroupPwadInfo &wi = postPWads.alloc();
1391 wi.filename = fname;
1392 wi.cosmetic = cosmetic;
1393 } else {
1394 AddAnyUserFile(fname, cosmetic);
1399 delete sc;
1403 //==========================================================================
1405 // AddGameAutoloads
1407 //==========================================================================
1408 static void AddGameAutoloads (VStr basedir, bool addAutoload=true) {
1409 if (fsys_onlyOneBaseFile) return;
1411 basedir = basedir.fixSlashes();
1412 if (basedir.length() == 0 || basedir == "/") return;
1413 if (addAutoload) basedir = basedir.appendPath("autoload");
1414 //GCon->Logf(NAME_Debug, "::: <%s> :::", *basedir);
1416 TArray<VStr> WadFiles;
1417 TArray<VStr> ZipFiles;
1419 // find all .wad/.pk3 files in that directory
1420 auto dirit = Sys_OpenDir(basedir);
1421 if (dirit) {
1422 for (VStr test = Sys_ReadDir(dirit); test.IsNotEmpty(); test = Sys_ReadDir(dirit)) {
1423 //fprintf(stderr, " <%s>\n", *test);
1424 if (test[0] == '_' || test[0] == '.') continue; // skip it
1425 VStr ext = test.ExtractFileExtension();
1426 if (ext.strEquCI(".wad")) WadFiles.Append(test);
1427 else if (ext.strEquCI(".pk3")) ZipFiles.Append(test);
1428 else if (ext.strEquCI(".vwad")) ZipFiles.Append(test);
1430 Sys_CloseDir(dirit);
1431 smsort_r(WadFiles.Ptr(), WadFiles.length(), sizeof(VStr), cmpfuncCINoExt, nullptr);
1432 smsort_r(ZipFiles.Ptr(), ZipFiles.length(), sizeof(VStr), cmpfuncCINoExt, nullptr);
1435 basedir = basedir.appendTrailingSlash();
1437 if (WadFiles.length() || ZipFiles.length()) {
1438 GCon->Logf(NAME_Init, "adding game autoloads from '%s'", *basedir);
1439 // now add wads, then pk3s
1440 for (auto &&fn : WadFiles) W_AddDiskFile(basedir.appendPath(fn), false);
1441 for (auto &&fn : ZipFiles) W_AddDiskFile(basedir.appendPath(fn));
1444 AddAutoloadRC(basedir);
1448 //==========================================================================
1450 // AddGameDir
1452 //==========================================================================
1453 static void AddGameDir (VStr basedir, VStr dir) {
1454 GCon->Logf(NAME_Init, "adding game dir '%s'", *dir);
1456 VStr bdx = basedir;
1457 if (bdx.length() == 0) bdx = "./";
1458 bdx = bdx.appendPath(dir);
1460 //GCon->Logf(NAME_Debug, "*** adding game dir \"%s\"", *bdx.quote());
1462 if (!Sys_DirExists(bdx)) return;
1464 fsys_hide_sprofs = modNoBaseSprOfs;
1466 TArray<VStr> WadFiles;
1467 TArray<VStr> ZipFiles;
1469 // find all .wad/.pk3 files in that directory
1470 auto dirit = Sys_OpenDir(bdx);
1471 if (dirit) {
1472 for (VStr test = Sys_ReadDir(dirit); test.IsNotEmpty(); test = Sys_ReadDir(dirit)) {
1473 //fprintf(stderr, " <%s>\n", *test);
1474 if (test[0] == '_' || test[0] == '.') continue; // skip it
1475 if (test.extractFileName().strEquCI("basepak.pk3")) continue; // it will be explicitly added later
1476 if (test.extractFileName().strEquCI("basepak.vwad")) continue; // it will be explicitly added later
1477 VStr ext = test.ExtractFileExtension();
1478 if (ext.strEquCI(".wad")) WadFiles.Append(test);
1479 else if (ext.strEquCI(".pk3")) ZipFiles.Append(test);
1480 else if (ext.strEquCI(".vwad")) ZipFiles.Append(test);
1482 Sys_CloseDir(dirit);
1483 smsort_r(WadFiles.Ptr(), WadFiles.length(), sizeof(VStr), cmpfuncCINoExt, nullptr);
1484 smsort_r(ZipFiles.Ptr(), ZipFiles.length(), sizeof(VStr), cmpfuncCINoExt, nullptr);
1487 // use `VStdFileStreamRead` so android port can override it
1488 auto bps = FL_OpenSysFileRead(bdx.appendPath("basepak.vwad"));
1489 if (bps) {
1490 delete bps;
1491 ZipFiles.insert(0, "basepak.vwad");
1492 GCon->Logf(NAME_Init, "found basepak at '%s'", *bdx.appendPath("basepak.vwad"));
1493 bps = FL_OpenSysFileRead(bdx.appendPath("basepak.pk3"));
1494 if (bps) {
1495 delete bps;
1496 Sys_Error("found both PK3 and VWAD basepacks");
1498 } else {
1499 bps = FL_OpenSysFileRead(bdx.appendPath("basepak.pk3"));
1500 if (bps) {
1501 delete bps;
1502 ZipFiles.insert(0, "basepak.pk3");
1503 GCon->Logf(NAME_Init, "found basepak at '%s'", *bdx.appendPath("basepak.pk3"));
1507 // add system dir, if it has any files
1508 if (ZipFiles.length() || WadFiles.length()) {
1509 wpkAppend(dir+"/", true); // don't strip path
1512 // now add wads, then pk3s
1513 for (int i = 0; i < WadFiles.length(); ++i) {
1514 //if (i == 0 && ZipFiles.length() == 0) wpkAppend(dir+"/"+WadFiles[i], true); // system pak
1515 W_AddDiskFile(bdx.appendPath(WadFiles[i]), false);
1518 for (int i = 0; i < ZipFiles.length(); ++i) {
1519 //if (i == 0) wpkAppend(dir+"/"+ZipFiles[i], true); // system pak
1520 bool isBPK = ZipFiles[i].extractFileName().strEquCI("basepak.pk3") ||
1521 ZipFiles[i].extractFileName().strEquCI("basepak.vwad");
1522 int spl = fsysSearchPaths.length();
1523 W_AddDiskFile(bdx.appendPath(ZipFiles[i]));
1524 if (isBPK) {
1525 // mark "basepak" flags
1526 for (int cc = spl; cc < fsysSearchPaths.length(); ++cc) {
1527 fsysSearchPaths[cc]->basepak = true;
1532 fsys_hide_sprofs = false;
1534 // custom mode
1535 SetupCustomMode(bdx);
1536 ApplyUserModes(dir);
1538 AddGameAutoloads(bdx);
1539 VStr gdn = dir.extractFileName();
1541 if (!gdn.isEmpty()) {
1542 #ifndef _WIN32
1543 const char *hdir = getenv("HOME");
1544 if (hdir && hdir[0] && hdir[1]) {
1545 VStr hbd = VStr(hdir);
1546 if (!gdn.isEmpty()) {
1547 hbd = hbd.appendPath(".k8vavoom");
1548 hbd = hbd.appendPath("autoload");
1549 hbd = hbd.appendPath(gdn);
1550 AddGameAutoloads(hbd, false);
1553 #endif
1554 for (auto &&audir : autoloadDirList) {
1555 VStr hbd = VStr(audir);
1556 if (hbd.isEmpty()) continue;
1557 if (hbd[0] == '!') {
1558 hbd.chopLeft(1);
1559 hbd = GParsedArgs.getBinDir()+hbd;
1560 } else if (hbd[0] == '~') {
1561 hbd.chopLeft(1);
1562 const char *homdir = getenv("HOME");
1563 if (homdir && homdir[0]) {
1564 hbd = VStr(homdir)+hbd;
1565 } else {
1566 continue;
1569 hbd = hbd.appendPath(gdn);
1570 //GCon->Logf(NAME_Debug, "!!! <%s> : <%s> : <%s>", *audir, *gdn, *hbd);
1571 AddGameAutoloads(hbd, false);
1575 // finally add directory itself
1576 // k8: nope
1578 VFilesDir *info = new VFilesDir(bdx);
1579 fsysSearchPaths.Append(info);
1584 //==========================================================================
1586 // AddGameDir
1588 //==========================================================================
1589 static void AddGameDir (VStr dir) {
1590 AddGameDir(fl_basedir, dir);
1591 //k8:wtf?!
1592 //if (fl_savedir.IsNotEmpty()) AddGameDir(fl_savedir, dir);
1593 fl_gamedir = dir;
1597 //==========================================================================
1599 // FindMainWad
1601 //==========================================================================
1602 static VStr FindMainWad (VStr MainWad) {
1603 if (MainWad.length() == 0) return VStr();
1605 if (Sys_FileExists(MainWad)) return MainWad;
1607 // if we have path separators, try relative path first
1608 #ifdef _WIN32
1609 const bool hasSep = (strchr(*MainWad, '\\') || strchr(*MainWad, '/') || (MainWad.length() >= 2 && MainWad[1] == ':'));
1610 #else
1611 const bool hasSep = !!strchr(*MainWad, '/');
1612 #endif
1614 //GLog.Logf(NAME_Debug, "trying to find iwad \"%s\" (hasSep=%d)", *MainWad.quote(), (int)hasSep);
1616 if (hasSep) {
1617 VStr nwadfname = Sys_FindFileCI(MainWad);
1618 if (nwadfname.length() != 0) return nwadfname;
1621 // first check in IWAD directories
1622 for (auto &&dir : IWadDirs) {
1623 //GLog.Logf(NAME_Debug, " looking for iwad '%s/%s'", *dir, *MainWad);
1624 VStr wadfname = Sys_FindFileCI(dir+"/"+MainWad);
1625 if (wadfname.length() != 0) {
1626 //GLog.Logf(NAME_Debug, " FOUND iwad \"%s/%s\"!", *dir, *MainWad);
1627 return wadfname;
1631 // then look in the save directory
1632 //if (fl_savedir.IsNotEmpty() && Sys_FileExists(fl_savedir+"/"+MainWad)) return fl_savedir+"/"+MainWad;
1634 // finally in base directory
1635 VStr bdwadfname = Sys_FindFileCI(fl_basedir+"/"+MainWad);
1636 if (bdwadfname.length() != 0) return bdwadfname;
1638 // just in case, check it as-is
1639 bdwadfname = Sys_FindFileCI(MainWad);
1640 if (bdwadfname.length() != 0) return bdwadfname;
1642 return VStr();
1646 //==========================================================================
1648 // SetupGameDir
1650 //==========================================================================
1651 static void SetupGameDir (VStr dirname) {
1652 AddGameDir(dirname);
1656 //==========================================================================
1658 // ParseStringValueOrList
1660 //==========================================================================
1661 static void ParseStringValueOrList (VScriptParser *sc, TArray<VStr> &list, VStr *dsc=nullptr) {
1662 sc->Expect("=");
1663 if (sc->Check("{")) {
1664 // parse list
1665 while (!sc->Check("}")) {
1666 sc->ExpectString();
1667 list.append(sc->String);
1668 if (!sc->Check(",")) {
1669 sc->Expect("}");
1670 break;
1673 if (dsc && sc->Check(":")) {
1674 sc->ExpectString();
1675 *dsc = sc->String;
1677 sc->Check(";"); // optional
1678 } else {
1679 // single value
1680 sc->ExpectString();
1681 list.append(sc->String);
1682 if (dsc && sc->Check(":")) {
1683 sc->ExpectString();
1684 *dsc = sc->String;
1686 sc->Expect(";");
1691 //==========================================================================
1693 // ParseStringValue
1695 //==========================================================================
1696 static VStr ParseStringValue (VScriptParser *sc) {
1697 sc->Expect("=");
1698 sc->ExpectString();
1699 VStr res = sc->String;
1700 sc->Expect(";");
1701 return res;
1705 //==========================================================================
1707 // ParseBoolValue
1709 //==========================================================================
1710 static bool ParseBoolValue (VScriptParser *sc) {
1711 sc->Expect("=");
1712 bool res = false;
1713 if (sc->Check("true") || sc->Check("tan")) res = true;
1714 else if (sc->Check("false") || sc->Check("ona")) res = false;
1715 else sc->Error("boolean expected");
1716 sc->Expect(";");
1717 return res;
1721 //==========================================================================
1723 // ParseGameDef
1725 // "{" already eaten
1727 //==========================================================================
1728 static void ParseGameDef (VScriptParser *sc, GameDefinition &game) {
1729 while (!sc->Check("}")) {
1730 // description
1731 if (sc->Check("description")) { game.description = ParseStringValue(sc); continue; }
1732 // game directory
1733 if (sc->Check("game")) { game.GameDir = ParseStringValue(sc); continue; }
1734 // base dirs
1735 if (sc->Check("base")) {
1736 game.BaseDirs.clear();
1737 ParseStringValueOrList(sc, game.BaseDirs);
1738 continue;
1740 // iwad
1741 if (sc->Check("iwad")) {
1742 TArray<VStr> iwads; // 2nd and next iwads will be added to addfiles
1743 VStr dsc;
1744 ParseStringValueOrList(sc, iwads, &dsc);
1745 if (iwads.length() == 0) continue;
1746 if (iwads[0].isEmpty()) sc->Error(va("game '%s' has empty main wad", *game.gamename));
1747 MainWadFiles &wf = game.mainWads.alloc();
1748 wf.main = iwads[0];
1749 wf.description = dsc;
1750 for (int f = 1; f < iwads.length(); ++f) {
1751 VStr s = iwads[f];
1752 if (s.isEmpty()) continue;
1753 if (s.length() == 1 && s[0] == '?') continue;
1754 VStr nfn = s;
1755 if (nfn[0] == '?') nfn.chopLeft(1);
1756 bool found = false;
1757 for (auto &&ks : wf.aux) {
1758 if (ks[0] == '?') {
1759 if (ks.mid(1, ks.length()).strEquCI(nfn)) { found = true; break; }
1760 } else {
1761 if (ks.strEquCI(nfn)) { found = true; break; }
1764 if (found) sc->Error(va("game '%s' has duplicate additional wad \"%s\"", *game.gamename, *s));
1765 wf.aux.append(s);
1767 continue;
1769 // CLI params
1770 if (sc->Check("param")) {
1771 game.params.clear();
1772 ParseStringValueOrList(sc, game.params);
1773 continue;
1775 // fix voices?
1776 if (sc->Check("fixvoices")) {
1777 game.FixVoices = ParseBoolValue(sc);
1778 continue;
1780 // warp template
1781 if (sc->Check("warp")) {
1782 game.warp = ParseStringValue(sc);
1783 continue;
1785 // filters
1786 if (sc->Check("filter")) {
1787 TArray<VStr> filters;
1788 ParseStringValueOrList(sc, filters);
1789 game.filters.clear();
1790 for (auto &&fs : filters) game.filters.append(VStr("filter/")+fs.toLowerCase());
1791 continue;
1793 // special flag
1794 if (sc->Check("ashexen")) {
1795 game.options.hexenGame = ParseBoolValue(sc);
1796 continue;
1798 // list of defines
1799 if (sc->Check("define")) {
1800 game.defines.clear();
1801 ParseStringValueOrList(sc, game.defines);
1802 continue;
1804 // warnings
1805 if (sc->Check("warnings")) {
1806 sc->Expect("=");
1807 sc->Expect("{");
1808 while (!sc->Check("}")) {
1809 sc->ExpectString();
1810 VStr wname = sc->String;
1811 const bool val = ParseBoolValue(sc);
1812 if (wname.strEquCI("dehacked")) game.options.warnDeh = val;
1813 else if (wname.strEquCI("pnames")) game.options.warnPNames = val;
1814 else if (wname.strEquCI("animated")) game.options.warnAnimated = val;
1816 continue;
1818 // options
1819 if (sc->Check("nakedbase")) {
1820 game.options.nakedbase = ParseBoolValue(sc);
1821 continue;
1823 if (sc->Check("bdw")) {
1824 game.options.bdw = (ParseBoolValue(sc) ? 1 : 0);
1825 continue;
1827 if (sc->Check("gore")) {
1828 game.options.gore = (ParseBoolValue(sc) ? 1 : 0);
1829 continue;
1831 if (sc->Check("modblood")) {
1832 game.options.modblood = (ParseBoolValue(sc) ? 1 : 0);
1833 continue;
1835 if (sc->Check("ludogibs")) {
1836 game.options.ludogibs = (ParseBoolValue(sc) ? 1 : 0);
1837 continue;
1839 if (sc->Check("sprofslump")) {
1840 game.options.sprofslump = ParseBoolValue(sc);
1841 continue;
1843 if (sc->Check("acstype")) {
1844 sc->Expect("=");
1845 if (sc->Check("default")) game.options.acsType = FL_ACS_Default;
1846 else if (sc->Check("zandronum") || sc->Check("zandro")) game.options.acsType = FL_ACS_Zandronum;
1847 else if (sc->Check("zdoom")) game.options.acsType = FL_ACS_ZDoom;
1848 else sc->Error(va("unknown asc type '%s'", *sc->String));
1849 sc->Expect(";");
1850 continue;
1852 // unknown shit
1853 if (!sc->GetString()) sc->Error("unexpected end of file");
1854 sc->Error(va("unknown command: '%s'", *sc->String));
1857 if (game.params.length() == 0) game.params.append(game.gamename);
1858 if (game.mainWads.length() == 0) sc->Error(va("game '%s' has no iwads", *game.gamename));
1859 if (game.GameDir.isEmpty()) sc->Error(va("game '%s' has no game dir", *game.gamename));
1861 // fix iwad descritions
1862 for (auto &&wfi : game.mainWads) {
1863 if (wfi.description.isEmpty()) wfi.description = game.description;
1868 //==========================================================================
1870 // ParseGameDefinitions
1872 //==========================================================================
1873 static void ParseGameDefinitions (VScriptParser *sc, TArray<GameDefinition> &games) {
1874 sc->SetCMode(true);
1875 while (!sc->AtEnd()) {
1876 if (sc->Check(";")) continue;
1877 if (sc->Check("game")) {
1878 sc->ExpectString();
1879 VStr gname = sc->String;
1880 if (gname.isEmpty()) sc->Error("game name is empty");
1881 for (int f = 0; f < games.length(); ) {
1882 if (games[f].gamename.strEqu(gname)) {
1883 games.removeAt(f);
1884 } else {
1885 ++f;
1888 sc->Expect("{");
1889 GameDefinition &game = games.Alloc();
1890 game.FixVoices = false;
1891 game.gamename = gname;
1892 ParseGameDef(sc, game);
1893 continue;
1896 delete sc;
1900 //==========================================================================
1902 // ParseBaseGameDefsFile
1904 //==========================================================================
1905 static void ParseBaseGameDefsFile (VStr name, VStr /*mainiwad*/, bool inHomeDir) {
1906 VStr UseName;
1908 if (name.isAbsolutePath()) {
1909 if (!Sys_FileExists(name)) return;
1910 UseName = name;
1911 } else if (inHomeDir) {
1912 if (fl_configdir.IsNotEmpty() && Sys_FileExists(fl_configdir+"/"+name)) {
1913 UseName = fl_configdir+"/"+name;
1914 } else {
1915 return;
1917 } else {
1918 if (Sys_FileExists(fl_basedir+"/"+name)) {
1919 UseName = fl_basedir+"/"+name;
1920 } else {
1921 return;
1925 if (dbg_dump_gameinfo || inHomeDir || true) GCon->Logf(NAME_Init, "Parsing game definition file \"%s\"", *UseName);
1926 VScriptParser *sc = new VScriptParser(UseName, FL_OpenSysFileRead(UseName));
1927 ParseGameDefinitions(sc, knownGames);
1928 if (dbg_dump_gameinfo) GCon->Logf(NAME_Init, "Done parsing game definition file \"%s\"", *UseName);
1932 //==========================================================================
1934 // ProcessBaseGameDefs
1936 //==========================================================================
1937 static void ProcessBaseGameDefs (VStr mainiwad) {
1938 GameDefinition *selectedGame = nullptr;
1940 if (knownGames.length() == 0) Sys_Error("No game definitions found!");
1942 for (auto &&game : knownGames) {
1943 for (auto &&arg : game.params) {
1944 if (arg.isEmpty()) continue;
1945 if (cliGameMode.strEquCI(arg)) {
1946 selectedGame = &game;
1947 break;
1950 if (selectedGame) break;
1953 if (selectedGame) {
1954 VStr gn = selectedGame->gamename;
1955 if (dbg_dump_gameinfo) GCon->Logf(NAME_Init, "SELECTED GAME: \"%s\"", *gn);
1956 } else {
1957 if (knownGames.length() > 1) {
1958 // try to detect game by custom iwad
1959 if (!selectedGame && mainiwad.length() > 0) {
1960 for (auto &&game : knownGames) {
1961 VStr mw = mainiwad.extractFileBaseName();
1962 for (auto &&mwi : game.mainWads) {
1963 VStr gw = mwi.main.extractFileBaseName();
1964 if (gw.strEquCI(mw)) {
1965 GCon->Logf(NAME_Init, "Detected game is '%s' (from iwad)", *mwi.description);
1966 selectedGame = &game;
1967 break;
1970 if (selectedGame) break;
1974 // try to select DooM or DooM II automatically
1975 if (!selectedGame) {
1976 //!performPWadScan();
1977 if (!pwadScanInfo.iwad.isEmpty()) {
1978 for (auto &&game : knownGames) {
1979 for (auto &&mwi : game.mainWads) {
1980 VStr gw = mwi.main.extractFileBaseName();
1981 if (gw.strEquCI(pwadScanInfo.iwad)) {
1982 GCon->Logf(NAME_Init, "Detected game is '%s' (from gameinfo)", *mwi.description);
1983 selectedGame = &game;
1984 break;
1987 if (selectedGame) break;
1991 // try to guess from map name
1992 if (!selectedGame) {
1993 //!performPWadScan();
1994 //GCon->Logf(NAME_Debug, "*** hasMapinfo: %d; mapname=<%s>; episode=%d; map=%d; index=%d", (int)pwadScanInfo.hasMapinfo, *pwadScanInfo.mapname, pwadScanInfo.episode, pwadScanInfo.mapnum, pwadScanInfo.getMapIndex());
1995 if (pwadScanInfo.getMapIndex() > 0) {
1996 //GCon->Logf("MNAME: <%s>", *mname);
1997 // found map, find DooM or DooM II game definition
1998 VStr gn1 = (pwadScanInfo.episode == 0 ? "doom2" : "doom");
1999 VStr gn2 = (pwadScanInfo.episode == 0 ? "freedoom2" : "freedoom");
2000 for (auto &&game : knownGames) {
2001 if (!gn1.strEquCI(game.gamename) && !gn2.strEquCI(game.gamename)) continue;
2002 // check if we have the corresponding iwad
2003 VStr mwp;
2004 for (auto &&mwi : game.mainWads) {
2005 mwp = FindMainWad(mwi.main);
2006 if (!mwp.isEmpty()) {
2007 if (!pwadScanInfo.maplump.isEmpty()) {
2008 GCon->Logf(NAME_Init, "Detected game is '%s' (from map lump '%s' at '%s')", *mwi.description, *pwadScanInfo.mapname, *pwadScanInfo.maplump);
2009 } else {
2010 GCon->Logf(NAME_Init, "Detected game is '%s' (from map lump '%s')", *mwi.description, *pwadScanInfo.mapname);
2012 selectedGame = &game;
2013 break;
2016 if (selectedGame) break;
2022 // try to find game iwad
2023 if (!selectedGame) {
2024 for (auto &&game : knownGames) {
2025 VStr mainWadPath;
2026 for (auto &&mwi : game.mainWads) {
2027 mainWadPath = FindMainWad(mwi.main);
2028 if (!mainWadPath.isEmpty()) {
2029 GCon->Logf(NAME_Init, "Detected game is '%s' (iwad search)", *mwi.description);
2030 selectedGame = &game;
2031 break;
2034 if (selectedGame) break;
2039 if (selectedGame >= 0) {
2040 game_name = *knownGames[selectedGame].gamename;
2041 GCon->Logf(NAME_Init, "detected game: \"%s\"", *knownGames[selectedGame].gamename);
2044 } else {
2045 selectedGame = &knownGames[0];
2047 if (!selectedGame) {
2048 if (mainiwad.isEmpty()) Sys_Error("Looks like I cannot find any IWADs. Did you forgot to specify -iwaddir?");
2049 selectedGame = &knownGames[0];
2052 // set game name variable
2053 game_name = *selectedGame->gamename;
2055 vassert(selectedGame);
2056 GameDefinition &game = *selectedGame;
2057 game_options = game.options;
2059 for (auto &&ds : game.defines) {
2060 if (!ds.isEmpty()) {
2061 VMemberBase::StaticAddDefine(*ds);
2062 GCon->Logf(NAME_Init, "added define '%s' for game '%s'", *ds, *game.gamename);
2066 // look for the main wad file
2067 VStr mainWadPath;
2069 int iwadidx = -1;
2070 VStr gameDsc;
2071 if (!modDetectorDisabledIWads) {
2072 // try user-specified iwad
2073 if (mainiwad.length() > 0) {
2074 GCon->Logf(NAME_Init, "trying custom IWAD '%s'", *mainiwad);
2075 mainWadPath = FindMainWad(mainiwad);
2076 if (mainWadPath.isEmpty()) Sys_Error("custom IWAD '%s' not found!", *mainiwad);
2077 GCon->Logf(NAME_Init, "found custom IWAD '%s'", *mainWadPath);
2078 gameDsc = game.description;
2079 } else {
2080 // try default iwads
2081 for (iwadidx = 0; iwadidx < game.mainWads.length(); ++iwadidx) {
2082 mainWadPath = FindMainWad(game.mainWads[iwadidx].main);
2083 if (!mainWadPath.isEmpty()) {
2084 gameDsc = game.mainWads[iwadidx].description;
2085 break;
2088 if (mainWadPath.isEmpty()) Sys_Error("Main wad file \"%s\" not found.", *game.mainWads[0].main);
2089 vassert(iwadidx >= 0 && iwadidx < game.mainWads.length());
2091 } else {
2092 gameDsc = "custom TC";
2095 // process filters and warp template
2096 FL_ClearGameFilters();
2097 for (auto &&flt : game.filters) {
2098 int res = FL_AddGameFilter(flt);
2099 switch (res) {
2100 case FL_ADDFILTER_OK: break;
2101 case FL_ADDFILTER_INVALID: GCon->Logf(NAME_Error, "Invalid game filter '%s', ignored", *flt); break;
2102 case FL_ADDFILTER_DUPLICATE: GCon->Logf(NAME_Warning, "Duplicate game filter '%s', ignored", *flt); break;
2103 default: GCon->Logf(NAME_Error, "Unknown game filter error %d for filter '%s', ignored", res, *flt); break;
2106 warpTpl = game.warp;
2108 // set options
2109 if (game_options.nakedbase) { GCon->Log(NAME_Init, "iwad forced 'nakedbase'"); mdetect_ClearAndBlockCustomModes(); }
2110 if (game_options.acsType != FL_ACS_Default) { GCon->Log(NAME_Init, "iwad forced ACS type"); flACSType = game_options.acsType; }
2111 if (game_options.bdw >= 0) { GCon->Logf(NAME_Init, "iwad forced BDW (%s)", (game_options.bdw == 0 ? "off" : "on")); fsys_DisableBDW = (game_options.bdw == 0); cli_BDWMod = game_options.bdw; }
2112 if (game_options.gore >= 0) {
2113 if (cli_GoreMod != game_options.gore) GCon->Logf(NAME_Init, "iwad forced gore mod (%s).", (game_options.gore ? "on" : "off"));
2114 cli_GoreMod = game_options.gore;
2116 if (game_options.modblood >= 0) {
2117 //if (game_options.modblood < 0) game_options.modblood = (cli_GoreMod == 0);
2118 fsys_DisableBloodReplacement = (game_options.modblood == 0);
2120 if (game_options.ludogibs >= 0) { GCon->Logf(NAME_Init, "iwad forced Ludicruous Gibs (%s)", (game_options.ludogibs == 0 ? "off" : "on")); cli_LudoGibsMod = game_options.ludogibs; }
2121 if (!game_options.sprofslump) modNoBaseSprOfs = true;
2123 // append iwad
2124 //GCon->Logf("MAIN WAD(1): '%s'", *MainWadPath);
2126 GCon->Logf(NAME_Init, "loading game %s", *gameDsc);
2127 bool iwadAdded = false;
2128 if (!modDetectorDisabledIWads) {
2129 // if iwad is pk3, add it last
2130 if (mainWadPath.endsWithCI("wad")) {
2131 GCon->Logf(NAME_Init, "adding iwad \"%s\"", *mainWadPath);
2132 iwadAdded = true;
2133 IWadIndex = fsysSearchPaths.length();
2134 wpkAppend(mainWadPath, false); // mark iwad as "non-system" file, so path won't be stored in savegame
2135 AddAnyFile(mainWadPath, false, game.FixVoices);
2136 } else {
2137 GCon->Logf(NAME_Init, "using iwad \"%s\"", *mainWadPath);
2141 // add optional files
2142 if (iwadidx >= 0) {
2143 for (auto &&xfn : game.mainWads[iwadidx].aux) {
2144 vassert(!xfn.isEmpty());
2145 bool optional = (xfn[0] == '?');
2146 VStr fname = FindMainWad(optional ? xfn.mid(1, xfn.length()) : xfn);
2147 if (fname.isEmpty()) {
2148 if (optional) continue;
2149 Sys_Error("Required file \"%s\" not found", *xfn);
2151 GCon->Logf(NAME_Init, "additing game file \"%s\"", *fname);
2152 wpkAppend(fname, false); // mark additional files as "non-system", so path won't be stored in savegame
2153 AddAnyFile(fname, false);
2157 //GCon->Logf(NAME_Debug, "BDIRS: %d", game.BaseDirs.length());
2158 for (auto &&bdir : game.BaseDirs) if (!bdir.isEmpty()) AddGameDir(bdir);
2160 SetupGameDir(game.GameDir);
2162 // add iwad here
2163 if (!modDetectorDisabledIWads) {
2164 if (!iwadAdded) {
2165 GCon->Logf(NAME_Init, "adding iwad \"%s\"", *mainWadPath);
2166 IWadIndex = fsysSearchPaths.length();
2167 wpkAppend(mainWadPath, false); // mark iwad as "non-system" file, so path won't be stored in savegame
2168 AddAnyFile(mainWadPath, false, game.FixVoices);
2172 knownGames.clear();
2176 //==========================================================================
2178 // RenameSprites
2180 //==========================================================================
2181 static void RenameSprites () {
2182 VStream *Strm = FL_OpenFileRead("sprite_rename.txt");
2183 if (!Strm) return;
2185 VScriptParser *sc = new VScriptParser("sprite_rename.txt", Strm);
2186 TArray<VSpriteRename> Renames;
2187 TArray<VSpriteRename> AlwaysRenames;
2188 TArray<VLumpRename> LumpRenames;
2189 TArray<VLumpRename> AlwaysLumpRenames;
2190 while (!sc->AtEnd()) {
2191 bool Always = sc->Check("always");
2193 if (sc->Check("lump")) {
2194 sc->ExpectString();
2195 VStr Old = sc->String.ToLower();
2196 sc->ExpectString();
2197 VStr New = sc->String.ToLower();
2198 VLumpRename &R = (Always ? AlwaysLumpRenames.Alloc() : LumpRenames.Alloc());
2199 R.Old = *Old;
2200 R.New = *New;
2201 continue;
2204 sc->ExpectString();
2205 if (sc->String.Length() != 4) sc->Error("Sprite name must be 4 chars long");
2206 VStr Old = sc->String.ToLower();
2208 sc->ExpectString();
2209 if (sc->String.Length() != 4) sc->Error("Sprite name must be 4 chars long");
2210 VStr New = sc->String.ToLower();
2212 VSpriteRename &R = Always ? AlwaysRenames.Alloc() : Renames.Alloc();
2213 R.Old[0] = Old[0];
2214 R.Old[1] = Old[1];
2215 R.Old[2] = Old[2];
2216 R.Old[3] = Old[3];
2217 R.New[0] = New[0];
2218 R.New[1] = New[1];
2219 R.New[2] = New[2];
2220 R.New[3] = New[3];
2222 delete sc;
2224 bool RenameAll = !!cli_oldSprites;
2225 for (int i = 0; i < fsysSearchPaths.length(); ++i) {
2226 if (RenameAll || i == IWadIndex) fsysSearchPaths[i]->RenameSprites(Renames, LumpRenames);
2227 fsysSearchPaths[i]->RenameSprites(AlwaysRenames, AlwaysLumpRenames);
2232 // ////////////////////////////////////////////////////////////////////////// //
2235 //==========================================================================
2237 // countFmtHash
2239 //==========================================================================
2240 static int countFmtHash (VStr str) {
2241 if (str.length() == 0) return 0;
2242 int count = 0;
2243 bool inHash = false;
2244 for (const char *s = *str; *s; ++s) {
2245 if (*s == '#') {
2246 if (!inHash) ++count;
2247 inHash = true;
2248 } else {
2249 inHash = false;
2252 return count;
2256 //==========================================================================
2258 // cliFnameCollector
2260 //==========================================================================
2261 static int cliFnameCollector (VArgs &args, int idx, bool first) {
2262 //pwflag_SkipSaveList = 0;
2263 //bool first = true;
2264 //++idx; // done by the caller
2265 while (idx < args.Count()) {
2266 if (VStr::strEqu(args[idx], "-cosmetic")) {
2267 pwflag_SkipSaveList = 1;
2268 ++idx;
2269 continue;
2271 if (VParsedArgs::IsArgBreaker(args, idx)) break;
2272 VStr fname = args[idx++];
2273 const bool sts = !pwflag_SkipSaveList;
2274 pwflag_SkipSaveList = 0; // autoreset
2275 //GCon->Logf(NAME_Debug, "idx=%d; fname=<%s>; sts=%d", idx-1, *fname, (int)sts);
2276 if (fname.isEmpty()) { first = false; continue; }
2278 PWadFile pwf;
2279 pwf.fname = fname;
2280 pwf.skipSounds = !!pwflag_SkipSounds;
2281 pwf.skipSprites = !!pwflag_SkipSprites;
2282 pwf.skipDehacked = !!pwflag_SkipDehacked;
2283 pwf.storeInSave = sts;
2284 pwf.asDirectory = false;
2285 //GCon->Logf(NAME_Debug, "<%s>: sdh=%d", *fname, pwflag_SkipDehacked);
2287 if (Sys_DirExists(fname)) {
2288 pwf.asDirectory = true;
2289 if (!first) {
2290 GCon->Logf(NAME_Warning, "To mount directory '%s' as emulated PK3 file, you should use \"-file\".", *fname);
2291 } else {
2292 pwadList.append(pwf);
2294 } else if (Sys_FileExists(fname)) {
2295 pwadList.append(pwf);
2296 } else {
2297 GCon->Logf(NAME_Warning, "File \"%s\" doesn't exist.", *fname);
2299 first = false;
2301 return idx;
2305 //==========================================================================
2307 // FL_InitOptions
2309 //==========================================================================
2310 void FL_InitOptions () {
2311 fsys_IgnoreZScript = 0;
2313 GArgs.AddFileOption("!1-game"); // '!' means "has args, and breaking" (number is argc)
2314 GArgs.AddFileOption("!1-logfile"); // don't register log file in saves
2315 GArgs.AddFileOption("!1-iwad");
2316 GArgs.AddFileOption("!1-iwaddir");
2317 GArgs.AddFileOption("!1-basedir");
2318 GArgs.AddFileOption("!1-savedir");
2319 GArgs.AddFileOption("!1-configdir");
2320 GArgs.AddFileOption("!1-autoloaddir");
2321 GArgs.AddFileOption("!1-deh");
2322 GArgs.AddFileOption("!1-vc-decorate-ignore-file");
2323 GArgs.AddFileOption("!1-audio-device");
2325 FSYS_InitOptions(GParsedArgs);
2327 GParsedArgs.RegisterFlagSet("-c", "compile VavoomC/decorate code and immediately exit", &cli_CompileAndExit);
2329 GParsedArgs.RegisterFlagSet("-cosmetic", "!do not store next pwad in save list", &pwflag_SkipSaveList);
2331 GParsedArgs.RegisterFlagSet("-skip-sounds", "skip sounds in the following pwads", &pwflag_SkipSounds);
2332 GParsedArgs.RegisterFlagReset("-allow-sounds", "allow sounds in the following pwads", &pwflag_SkipSounds);
2333 // aliases
2334 GParsedArgs.RegisterAlias("-skipsounds", "-skip-sounds");
2335 GParsedArgs.RegisterAlias("-allowsounds", "-allow-sounds");
2337 GParsedArgs.RegisterFlagSet("-skip-sprites", "skip sprites in the following pwads", &pwflag_SkipSprites);
2338 GParsedArgs.RegisterFlagReset("-allow-sprites", "allow sprites in the following pwads", &pwflag_SkipSprites);
2339 // aliases
2340 GParsedArgs.RegisterAlias("-skipsprites", "-skip-sprites");
2341 GParsedArgs.RegisterAlias("-allowsprites", "-allow-sprites");
2343 GParsedArgs.RegisterFlagSet("-skip-dehacked", "skip dehacked in the following pwads", &pwflag_SkipDehacked);
2344 GParsedArgs.RegisterFlagReset("-allow-dehacked", "allow dehacked in the following pwads", &pwflag_SkipDehacked);
2345 // aliases
2346 GParsedArgs.RegisterAlias("-skipdehacked", "-skip-dehacked");
2347 GParsedArgs.RegisterAlias("-allowdehacked", "-allow-dehacked");
2349 GParsedArgs.RegisterFlagSet("-oldsprites", "!some sprite renaming crap (do not bother)", &cli_oldSprites);
2350 GParsedArgs.RegisterAlias("-old-sprites", "-oldsprites");
2352 GParsedArgs.RegisterFlagSet("-no-external-deh", "disable external (out-of-wad) .deh loading", &cli_NoExternalDeh);
2353 // aliases
2354 GParsedArgs.RegisterAlias("--no-extern-deh", "-no-external-deh");
2356 // filename collector
2357 GParsedArgs.RegisterCallback(nullptr, nullptr, [] (VArgs &args, int idx) -> int { return cliFnameCollector(args, idx, false); });
2358 GParsedArgs.RegisterCallback("-file", "add the following arguments as pwads", [] (VArgs &args, int idx) -> int { return cliFnameCollector(args, ++idx, true); });
2361 // modes collector
2362 GParsedArgs.RegisterCallback("-mode", "activate game mode from 'modes.rc'", [] (VArgs &args, int idx) -> int {
2363 ++idx;
2364 if (!VParsedArgs::IsArgBreaker(args, idx)) {
2365 VStr mn = args[idx++];
2366 if (!mn.isEmpty()) cliModesList.append(mn);
2368 return idx;
2372 // automatic groups collector
2373 GParsedArgs.RegisterCallback("-autoload", "activate game mode from 'autoload.rc'", [] (VArgs &args, int idx) -> int {
2374 ++idx;
2375 if (!VParsedArgs::IsArgBreaker(args, idx)) {
2376 VStr sg = args[idx++];
2377 if (!sg.isEmpty()) {
2378 GroupMask gi;
2379 gi.mask = sg;
2380 gi.enabled = true;
2381 if (gi.isGlob()) {
2382 cliGroupMask.append(gi);
2383 } else {
2384 cliGroupMap.put(sg.toLowerCase(), gi.enabled);
2388 return idx;
2390 GParsedArgs.RegisterAlias("-auto", "-autoload");
2391 GParsedArgs.RegisterAlias("-auto-load", "-autoload");
2393 GParsedArgs.RegisterStringArrayOption("-autoloaddir", "add autoload directory to the list of autoload dirs", &autoloadDirList);
2395 GParsedArgs.RegisterCallback("-skip-autoload", "skip game mode from 'autoload.rc'", [] (VArgs &args, int idx) -> int {
2396 ++idx;
2397 if (!VParsedArgs::IsArgBreaker(args, idx)) {
2398 VStr sg = args[idx++];
2399 if (!sg.isEmpty()) {
2400 GroupMask gi;
2401 gi.mask = sg;
2402 gi.enabled = false;
2403 if (gi.isGlob()) {
2404 cliGroupMask.append(gi);
2405 } else {
2406 cliGroupMap.put(sg.toLowerCase(), gi.enabled);
2410 return idx;
2412 GParsedArgs.RegisterAlias("-skipauto", "-skip-autoload");
2413 GParsedArgs.RegisterAlias("-skip-auto-load", "-skip-autoload");
2415 // game type
2416 GParsedArgs.RegisterStringOption("-game", "select game type", &cliGameCStr);
2418 // add known game aliases
2419 //FIXME: unhardcode this!
2420 GParsedArgs.RegisterCallback("-doom", "select Doom/Ultimate Doom game", [] (VArgs &/*args*/, int /*idx*/) -> int { cliGameCStr = nullptr; cliGameMode = "doom"; return 0; });
2421 GParsedArgs.RegisterAlias("-doom1", "-doom");
2422 GParsedArgs.RegisterCallback("-doom2", "select Doom II game", [] (VArgs &/*args*/, int /*idx*/) -> int { cliGameCStr = nullptr; cliGameMode = "doom2"; return 0; });
2423 GParsedArgs.RegisterCallback("-tnt", "select TNT Evilution game", [] (VArgs &/*args*/, int /*idx*/) -> int { cliGameCStr = nullptr; cliGameMode = "tnt"; return 0; });
2424 GParsedArgs.RegisterAlias("-evilution", "-tnt");
2425 GParsedArgs.RegisterCallback("-plutonia", "select Plutonia game", [] (VArgs &/*args*/, int /*idx*/) -> int { cliGameCStr = nullptr; cliGameMode = "plutonia"; return 0; });
2426 GParsedArgs.RegisterCallback("-nerve", "select Doom II + No Rest for the Living game", [] (VArgs &/*args*/, int /*idx*/) -> int { cliGameCStr = nullptr; cliGameMode = "nerve"; return 0; });
2428 GParsedArgs.RegisterCallback("-freedoom", "select Freedoom Phase I game", [] (VArgs &/*args*/, int /*idx*/) -> int { cliGameCStr = nullptr; cliGameMode = "freedoom"; return 0; });
2429 GParsedArgs.RegisterAlias("-freedoom1", "-freedoom");
2430 GParsedArgs.RegisterCallback("-freedoom2", "select Freedoom Phase II game", [] (VArgs &/*args*/, int /*idx*/) -> int { cliGameCStr = nullptr; cliGameMode = "freedoom2"; return 0; });
2432 GParsedArgs.RegisterCallback("-heretic", "select Heretic game", [] (VArgs &/*args*/, int /*idx*/) -> int { cliGameCStr = nullptr; cliGameMode = "heretic"; return 0; });
2433 GParsedArgs.RegisterCallback("-hexen", "select Hexen game", [] (VArgs &/*args*/, int /*idx*/) -> int { cliGameCStr = nullptr; cliGameMode = "hexen"; return 0; });
2434 GParsedArgs.RegisterCallback("-hexendd", "select Hexen:Deathkings game", [] (VArgs &/*args*/, int /*idx*/) -> int { cliGameCStr = nullptr; cliGameMode = "hexendd"; return 0; });
2436 GParsedArgs.RegisterCallback("-strife", "select Strife game", [] (VArgs &/*args*/, int /*idx*/) -> int { cliGameCStr = nullptr; cliGameMode = "strife"; return 0; });
2437 GParsedArgs.RegisterCallback("-strifeteaser", "select Strife Teaser game", [] (VArgs &/*args*/, int /*idx*/) -> int { cliGameCStr = nullptr; cliGameMode = "strifeteaser"; return 0; });
2439 // hidden
2440 GParsedArgs.RegisterCallback("-complete", "!DooM Complete game (broken for now)", [] (VArgs &/*args*/, int /*idx*/) -> int { cliGameCStr = nullptr; cliGameMode = "complete"; return 0; });
2442 GParsedArgs.RegisterCallback("-chex", "Chex Quest game (semi-broken)", [] (VArgs &/*args*/, int /*idx*/) -> int { cliGameCStr = nullptr; cliGameMode = "chex"; return 0; });
2443 GParsedArgs.RegisterAlias("-chex1", "-chex");
2444 GParsedArgs.RegisterCallback("-chex2", "Chex Quest 2 game (semi-broken)", [] (VArgs &/*args*/, int /*idx*/) -> int { cliGameCStr = nullptr; cliGameMode = "chex2"; return 0; });
2445 GParsedArgs.RegisterCallback("-chex3", "!Chex Quest 3 game (semi-broken)", [] (VArgs &/*args*/, int /*idx*/) -> int { cliGameCStr = nullptr; cliGameMode = "chex3"; return 0; });
2447 GParsedArgs.RegisterCallback("-hacx", "HacX (untested) game", [] (VArgs &/*args*/, int /*idx*/) -> int { cliGameCStr = nullptr; cliGameMode = "hacx"; return 0; });
2449 GParsedArgs.RegisterFlagSet("-k8runmap", "try to detect and run first pwad map automatically", &doStartMap);
2451 GParsedArgs.RegisterFlagSet("-fast", "fast monsters", &cli_FastMonsters);
2452 GParsedArgs.RegisterFlagReset("-slow", "slow monsters", &cli_FastMonsters);
2454 GParsedArgs.RegisterFlagSet("-respawn", "turn on respawning", &cli_Respawn);
2455 GParsedArgs.RegisterFlagReset("-no-respawn", "!reverse of \"-respawn\"", &cli_Respawn);
2457 GParsedArgs.RegisterFlagSet("-nomonsters", "disable monsters", &cli_NoMonsters);
2458 GParsedArgs.RegisterAlias("-no-monsters", "-nomonsters");
2459 GParsedArgs.RegisterFlagReset("-monsters", "!reverse of \"monsters\"", &cli_NoMonsters);
2461 GParsedArgs.RegisterFlagSet("-nomenudef", "disable gzdoom MENUDEF parsing", &cli_NoMenuDef);
2462 GParsedArgs.RegisterAlias("-no-menudef", "-nomenudef");
2463 GParsedArgs.RegisterFlagReset("-allow-menudef", "!reverse of \"-nomenudef\"", &cli_NoMenuDef);
2465 GParsedArgs.RegisterFlagSet("-gore", "enable gore mod", &cli_GoreMod);
2466 GParsedArgs.RegisterFlagReset("-nogore", "disable gore mod", &cli_GoreMod);
2467 GParsedArgs.RegisterAlias("-no-gore", "-nogore");
2469 GParsedArgs.RegisterFlagSet("-force-gore", "force gore mod", &cli_GoreModForce);
2471 GParsedArgs.RegisterFlagSet("-bdw", "enable BDW (weapons) mod", &cli_BDWMod);
2472 GParsedArgs.RegisterFlagReset("-nobdw", "disable BDW (weapons) mod", &cli_BDWMod);
2473 GParsedArgs.RegisterAlias("-no-bdw", "-nobdw");
2475 GParsedArgs.RegisterFlagSet("-gibs", "enable Ludicruous Gibs mod", &cli_LudoGibsMod);
2476 GParsedArgs.RegisterFlagReset("-nogibs", "disable Ludicruous Gibs mod", &cli_LudoGibsMod);
2477 GParsedArgs.RegisterAlias("-no-gibs", "-nogibs");
2479 // hidden
2480 GParsedArgs.RegisterFlagSet("-skeehud", "!force SkullDash EE HUD", &cli_SkeeHUD);
2482 // "-skill"
2483 GParsedArgs.RegisterCallback("-skill", "select starting skill (3 is HMP; default is UV aka 4)", [] (VArgs &args, int idx) -> int {
2484 ++idx;
2485 if (!VParsedArgs::IsArgBreaker(args, idx)) {
2486 int skn = M_SkillFromName(args[idx]);
2487 if (skn < 0) skn = 4-1; // default is UV
2488 Skill = skn;
2489 ++idx;
2490 } else {
2491 GCon->Log(NAME_Warning, "skill name expected!");
2493 return idx;
2496 GParsedArgs.RegisterStringOption("-iwad", "override iwad file name", &cli_IWadName);
2497 GParsedArgs.RegisterStringOption("-iwaddir", "set directory to look for iwads", &cli_IWadDir);
2498 GParsedArgs.RegisterStringOption("-basedir", "set directory to look for base k8vavoom pk3s", &cli_BaseDir);
2499 GParsedArgs.RegisterStringOption("-savedir", "set directory to store save files", &cli_SaveDir);
2500 GParsedArgs.RegisterStringOption("-configdir", "set directory to store config file (and save files)", &cli_ConfigDir);
2502 // "-warp"
2503 GParsedArgs.RegisterCallback("-warp", "warp to map number", [] (VArgs &args, int idx) -> int {
2504 ++idx;
2505 int wmap1 = -1, wmap2 = -1;
2506 bool mapok = false, epiok = false;
2507 if (!VParsedArgs::IsArgBreaker(args, idx)) {
2508 mapok = VStr::convertInt(args[idx], &wmap1);
2509 if (mapok) {
2510 ++idx;
2511 if (!VParsedArgs::IsArgBreaker(args, idx)) {
2512 epiok = VStr::convertInt(args[idx], &wmap2);
2513 if (epiok) ++idx;
2516 if (!mapok) wmap1 = -1;
2517 if (!epiok) wmap2 = -1;
2519 if (wmap1 < 0) wmap1 = -1;
2520 if (wmap2 < 0) wmap2 = -1;
2521 fsys_warp_n0 = wmap1;
2522 fsys_warp_n1 = wmap2;
2523 return idx;
2526 GParsedArgs.RegisterFlagReset("-silentwads", "do not report loaded wads (report by default)", &reportWads);
2528 GParsedArgs.RegisterFlagSet("-nakedbase", "skip all autoloads", &cli_NakedBase);
2529 GParsedArgs.RegisterAlias("-naked-base", "-nakedbase");
2531 // hidden
2532 GParsedArgs.RegisterFlagSet("-Wall", "!turn on various useless warnings", &cli_WAll);
2534 #ifdef _WIN32
2535 autoloadDirList.append("!/autoload");
2536 #endif
2540 //==========================================================================
2542 // FL_Init
2544 //==========================================================================
2545 void FL_Init () {
2546 const char *p;
2547 VStr mainIWad = VStr();
2549 // check for cmake dir
2550 #ifndef VAVOOM_K8_DEVELOPER
2552 VStr crapmakedir(GParsedArgs.getBinDir());
2553 if (Sys_DirExists(crapmakedir.appendPath("CMakeFiles")) ||
2554 Sys_FileExists(crapmakedir.appendPath("CMakeCache.txt")) ||
2555 Sys_FileExists(crapmakedir.appendPath("cmake_install.cmake")))
2557 Sys_Error("Please, do not run k8vavoom from build dir! Without proper `make install` k8vavoom will not work!");
2560 #endif
2562 FL_RegisterModDetectors();
2564 /* oops! they are already parsed here
2565 fsys_warp_n0 = -1;
2566 fsys_warp_n1 = -1;
2567 fsys_warp_cmd = VStr();
2570 // if it is set, than it was the latest
2571 if (cliGameCStr) cliGameMode = cliGameCStr;
2573 if (cli_FastMonsters == 1) fastparm = 1;
2574 else if (cli_FastMonsters == 0) fastparm = -1;
2576 if (cli_Respawn == 1) respawnparm = true;
2577 if (cli_NoMonsters == 1) NoMonsters = true;
2578 if (cli_NoMenuDef == 1) gz_skip_menudef = true;
2581 bool isChex = false;
2582 if (cliGameMode.strEquCI("chex")) {
2583 cliGameMode = "doom2"; // arbitrary
2584 //game_override_mode = GAME_Chex;
2585 fsys_onlyOneBaseFile = true; // disable autoloads
2586 isChex = true;
2590 if (cli_IWadName && cli_IWadName[0]) {
2591 mainIWad = cli_IWadName;
2592 if (mainIWad.length()) {
2593 if (!Sys_FileExists(mainIWad) && !mainIWad.IsAbsolutePath()) {
2594 // try "-iwaddir"
2595 if (cli_IWadDir && cli_IWadDir[0]) {
2596 VStr ipp = VStr(cli_IWadDir).appendPath(mainIWad);
2597 if (Sys_FileExists(ipp)) {
2598 mainIWad = ipp;
2599 GCon->Logf(NAME_Init, "found custom iwad at \"-iwaddir\"");
2600 vassert(Sys_FileExists(mainIWad));
2604 if (!Sys_FileExists(mainIWad)) Sys_Error("custom IWAD '%s' not found!", *mainIWad);
2608 fsys_report_added_paks = !!reportWads;
2610 //if (!isChex) fsys_onlyOneBaseFile = (cli_NakedBase > 0);
2611 fsys_onlyOneBaseFile = (cli_NakedBase > 0);
2613 // set up base directory (main data files)
2614 fl_basedir = ".";
2615 p = cli_BaseDir;
2616 if (p) {
2617 fl_basedir = p;
2618 if (fl_basedir.isEmpty()) fl_basedir = ".";
2619 } else {
2620 /*static*/ const char *defaultBaseDirs[] = {
2621 #ifdef __SWITCH__
2622 ".",
2623 "./share",
2624 #elif !defined(_WIN32)
2625 "/opt/vavoom/share/k8vavoom",
2626 "/opt/k8vavoom/share/k8vavoom",
2627 "/usr/local/share/k8vavoom",
2628 "/usr/share/k8vavoom",
2629 "!/../share/k8vavoom",
2630 ".",
2631 #else
2632 "!/share",
2633 "!/.",
2634 ".",
2635 #endif
2636 nullptr,
2638 for (const char **tbd = defaultBaseDirs; *tbd; ++tbd) {
2639 VStr dir = VStr(*tbd);
2640 if (dir[0] == '!') {
2641 dir.chopLeft(1);
2642 dir = GParsedArgs.getBinDir()+dir;
2643 } else if (dir[0] == '~') {
2644 dir.chopLeft(1);
2645 const char *hdir = getenv("HOME");
2646 if (hdir && hdir[0]) {
2647 dir = VStr(hdir)+dir;
2648 } else {
2649 continue;
2652 if (Sys_DirExists(dir)) {
2653 fl_basedir = dir;
2654 break;
2657 if (fl_basedir.isEmpty()) Sys_Error("cannot find basedir; use \"-basedir dir\" to set it");
2660 // set up config directory (files written by engine)
2661 if (cli_ConfigDir && cli_ConfigDir[0]) {
2662 fl_configdir = VStr(cli_ConfigDir).fixSlashes();
2663 } else {
2664 fl_configdir = ".";
2665 #if !defined(_WIN32)
2666 const char *HomeDir = getenv("HOME");
2667 if (HomeDir && HomeDir[0]) fl_configdir = VStr(HomeDir)+"/.k8vavoom";
2668 #else
2669 //fl_configdir = ".";
2670 fl_configdir = VStr(VArgs::GetBinaryDir());
2671 #endif
2673 fl_configdir = fl_configdir.removeTrailingSlash();
2675 // set up save directory (files written by engine)
2676 p = cli_SaveDir;
2677 if (p && p[0]) {
2678 fl_savedir = p;
2679 } else {
2680 fl_savedir = fl_configdir.appendPath("saves");
2682 fl_savedir = fl_savedir.removeTrailingSlash();
2684 // set up additional directories where to look for IWAD files
2685 p = cli_IWadDir;
2686 if (p) {
2687 if (p[0]) IWadDirs.Append(p);
2688 } else {
2689 /*static*/ const char *defaultIwadDirs[] = {
2690 ".",
2691 "!/.",
2692 #ifdef __SWITCH__
2693 "./iwads",
2694 #elif !defined(_WIN32)
2695 "~/.k8vavoom/iwads",
2696 "~/.k8vavoom/iwad",
2697 "~/.k8vavoom",
2698 "~/.vavoom/iwads",
2699 "~/.vavoom/iwad",
2700 "~/.vavoom",
2701 "/opt/vavoom/share/k8vavoom",
2702 "/opt/k8vavoom/share/k8vavoom",
2703 "/usr/local/share/k8vavoom",
2704 "/usr/share/k8vavoom",
2705 "!/../share/k8vavoom",
2706 #else
2707 "~",
2708 "!/iwads",
2709 "!/iwad",
2710 #endif
2711 nullptr,
2713 for (const char **tbd = defaultIwadDirs; *tbd; ++tbd) {
2714 VStr dir = VStr(*tbd);
2715 if (dir[0] == '!') {
2716 dir.chopLeft(1);
2717 dir = GParsedArgs.getBinDir()+dir;
2718 } else if (dir[0] == '~') {
2719 dir.chopLeft(1);
2720 const char *hdir = getenv("HOME");
2721 if (hdir && hdir[0]) {
2722 dir = VStr(hdir)+dir;
2723 } else {
2724 continue;
2727 if (Sys_DirExists(dir)) {
2728 //GLog.Logf(NAME_Debug, "found iwad dir '%s'", *dir);
2729 IWadDirs.Append(dir);
2733 // envvars: DOOMWADDIR
2735 const char *dwd = getenv("DOOMWADDIR");
2736 if (dwd && dwd[0]) IWadDirs.Append(dwd);
2738 // envvars: DOOMWADPATH ( https://doomwiki.org/wiki/Environment_variables )
2740 const char *dwp = getenv("DOOMWADPATH");
2741 if (dwp) {
2742 VStr s(dwp);
2743 s = s.trimAll();
2744 while (s.length()) {
2745 int p0 = s.indexOf(':');
2746 int p1 = s.indexOf(';');
2747 #ifdef _WIN32
2748 if (p0 == 1 && VStr::isAlphaAscii(s[0])) p0 = -1; // looks like 'a:'
2749 #endif
2750 if (p0 >= 0 && p1 >= 0) p0 = min2(p0, p1);
2751 else if (p1 >= 0) { vassert(p0 < 0); p0 = p1; }
2752 if (p0 < 0) p0 = s.length();
2753 VStr pt = s.left(p0).trimAll();
2754 s.chopLeft(p0+1);
2755 s = s.trimLeft();
2756 if (pt.length()) {
2757 //GLog.Logf(NAME_Debug, "DWP: <%.*s>", pt.length(), *pt);
2758 IWadDirs.Append(pt);
2763 #ifdef _WIN32
2764 // home dir (if any)
2767 const char *hd = getenv("HOME");
2768 if (hd && hd[0]) IWadDirs.Append(hd);
2771 #endif
2772 // and current dir
2773 //IWadDirs.Append(".");
2775 ParseDetectors("basev/detectors.txt", false);
2776 // parse detectors from home directory
2777 ParseDetectors("detectors.rc", true);
2779 ParseVWadKeys("basev/vwad_known_keys.txt", false);
2780 // parse keys from home directory
2781 ParseVWadKeys("vwad_known_keys.rc", true);
2783 #ifndef _WIN32
2786 const char *hdir = getenv("HOME");
2787 if (hdir && hdir[0]) {
2788 ParseDetectors(VStr(va("%s/.k8vavoom/detectors.rc", hdir)));
2792 #endif
2794 int mapnum = -1;
2795 VStr mapname;
2796 bool mapinfoFound = false;
2798 // mount pwads
2799 FL_StartUserWads(); // start marking
2800 for (int pwidx = 0; pwidx < pwadList.length(); ++pwidx) {
2801 PWadFile &pwf = pwadList[pwidx];
2802 fsys_skipSounds = pwf.skipSounds;
2803 fsys_skipSprites = pwf.skipSprites;
2804 fsys_skipDehacked = pwf.skipDehacked;
2805 //!int nextfid = W_NextMountFileId();
2807 //GCon->Logf(NAME_Debug, "::: %d : <%s>", nextfid, *pwf.fname);
2808 int currFCount = fsysSearchPaths.length();
2810 auto mark = wpkMark(!pwf.storeInSave);
2812 if (pwf.asDirectory) {
2813 //if (pwf.storeInSave) wpkAppend(pwf.fname, false); // non-system pak
2814 GCon->Logf(NAME_Init, "Mounting directory '%s' as emulated PK3 file.", *pwf.fname);
2815 //AddPakDir(pwf.fname);
2816 W_MountDiskDir(pwf.fname);
2817 } else {
2818 //if (pwf.storeInSave) wpkAppend(pwf.fname, false); // non-system pak
2819 AddAnyFile(pwf.fname, true);
2822 // ignore cosmetic pwads
2823 if (!pwf.storeInSave) {
2824 for (int f = currFCount; f < fsysSearchPaths.length(); ++f) fsysSearchPaths[f]->cosmetic = true;
2827 wpkAddMarked(mark);
2829 fsys_hasPwads = true;
2831 FL_EndUserWads(); // stop marking
2833 fsys_skipSounds = false;
2834 fsys_skipSprites = false;
2835 fsys_skipDehacked = false;
2837 // scan for user maps
2838 performPWadScan();
2839 if (pwadScanInfo.hasMapinfo) {
2840 mapinfoFound = true;
2841 fsys_hasMapPwads = true;
2842 } else if (!pwadScanInfo.mapname.isEmpty()) {
2843 mapnum = pwadScanInfo.getMapIndex();
2844 mapname = pwadScanInfo.mapname;
2845 fsys_hasMapPwads = true;
2848 // save pwads to be added later
2849 FSysSavedState pwadsSaved;
2850 pwadsSaved.save();
2851 TArray<VStr> wpklistSaved = wpklist;
2852 TArray<VStr> wpklistSavedSmall = wpklistSmall;
2853 wpklist.clear();
2854 wpklistSmall.clear();
2857 ParseUserModes();
2859 AddGameDir("basev/common");
2861 //collectPWads();
2863 ParseBaseGameDefsFile("basev/games.txt", mainIWad, false);
2864 ParseBaseGameDefsFile("games.rc", mainIWad, true); // load rc from user home directory too
2865 ProcessBaseGameDefs(mainIWad);
2867 // process "warp", do it here, so "+var" will be processed after "map nnn"
2868 // postpone, and use `P_TranslateMapEx()` in host initialization
2869 if (warpTpl.length() > 0 && fsys_warp_n0 >= 0) {
2870 int fmtc = countFmtHash(warpTpl);
2871 if (fmtc >= 1 && fmtc <= 2) {
2872 if (fmtc == 2 && fsys_warp_n1 == -1) { fsys_warp_n1 = fsys_warp_n0; fsys_warp_n0 = 1; } // "-warp n" is "-warp 1 n" for ExMx
2873 VStr cmd = "map ";
2874 int spos = 0;
2875 int numidx = 0;
2876 while (spos < warpTpl.length()) {
2877 if (warpTpl[spos] == '#') {
2878 int len = 0;
2879 while (spos < warpTpl.length() && warpTpl[spos] == '#') { ++len; ++spos; }
2880 char tbuf[64];
2881 snprintf(tbuf, sizeof(tbuf), "%d", (numidx == 0 ? fsys_warp_n0 : fsys_warp_n1));
2882 VStr n = VStr(tbuf);
2883 while (n.length() < len) n = VStr("0")+n;
2884 cmd += n;
2885 ++numidx;
2886 } else {
2887 cmd += warpTpl[spos++];
2890 cmd += "\n";
2891 //GCmdBuf.Insert(cmd);
2892 fsys_warp_cmd = cmd;
2893 fsys_warp_n0 = -1;
2894 fsys_warp_n1 = -1;
2898 //customMode.dump();
2900 if (cli_GoreModForce != 0) {
2901 GCon->Logf(NAME_Init, "Forcing gore mod.");
2902 //AddGameDir("basev/mods/gore"); // not disabled
2903 wpklist.append("basev/mods/gore/"); // to not invalidate saves
2904 //wpklistSmall.append("basev/mods/gore/"); // to not invalidate saves
2905 cli_GoreMod = cli_GoreModForce;
2906 k8gore_enabled_override = 1;
2907 k8gore_enabled_override_decal = 1;
2908 } else if (!customMode.disableGoreMod) {
2909 #if 0
2910 if (/*game_release_mode ||*/ isChex) {
2911 if (cli_GoreMod == 1) AddGameDir("basev/mods/gore"); // explicitly enabled
2912 } else {
2913 if (cli_GoreMod != 0) AddGameDir("basev/mods/gore"); // not disabled
2915 #else
2916 //if (cli_GoreMod != 0) AddGameDir("basev/mods/gore"); // not disabled
2917 if (cli_GoreMod == 0) {
2918 k8gore_enabled_override = -1;
2919 k8gore_enabled_override_decal = -1;
2920 } else {
2921 wpklist.append("basev/mods/gore/"); // to not invalidate saves
2922 //wpklistSmall.append("basev/mods/gore/"); // to not invalidate saves
2924 #endif
2925 } else {
2926 GCon->Logf(NAME_Init, "Gore mod disabled.");
2927 k8gore_enabled_override = -1;
2928 k8gore_enabled_override_decal = -1;
2931 //if (isChex) AddGameDir("basev/mods/chex");
2933 // mark "iwad" flags
2934 for (int i = 0; i < fsysSearchPaths.length(); ++i) {
2935 fsysSearchPaths[i]->iwad = true;
2938 // load custom mode pwads
2939 if (customMode.disableBloodReplacement) fsys_DisableBloodReplacement = true;
2940 if (customMode.disableBDW) fsys_DisableBDW = true;
2941 if (!customMode.ForcePlayerClass.isEmpty()) { GCon->Logf(NAME_Init, "user mode forces player class '%s'.", *customMode.ForcePlayerClass); flForcePlayerClass = customMode.ForcePlayerClass; }
2943 fsys_report_added_paks = !!reportWads;
2944 //GCon->Logf(NAME_Debug, "!!!: %d", (fsys_report_added_paks ? 1 : 0));
2946 CustomModeLoadPwads(CM_PRE_PWADS);
2948 // mount pwads (actually, add already mounted ones)
2949 if (pwadsSaved.needReload()) {
2950 GCon->Logf(NAME_Init, "some user archives had filters, reloading...");
2951 pwadsSaved.unload();
2953 // mount pwads
2954 FL_StartUserWads(); // start marking
2955 for (int pwidx = 0; pwidx < pwadList.length(); ++pwidx) {
2956 PWadFile &pwf = pwadList[pwidx];
2957 fsys_skipSounds = pwf.skipSounds;
2958 fsys_skipSprites = pwf.skipSprites;
2959 fsys_skipDehacked = pwf.skipDehacked;
2960 //!int nextfid = W_NextMountFileId();
2962 //GCon->Logf(NAME_Debug, "::: %d : <%s>", nextfid, *pwf.fname);
2963 int currFCount = fsysSearchPaths.length();
2965 auto mark = wpkMark(!pwf.storeInSave);
2967 if (pwf.asDirectory) {
2968 //if (pwf.storeInSave) wpkAppend(pwf.fname, false); // non-system pak
2969 GCon->Logf(NAME_Init, "Mounting directory '%s' as emulated PK3 file.", *pwf.fname);
2970 //AddPakDir(pwf.fname);
2971 W_MountDiskDir(pwf.fname);
2972 } else {
2973 //if (pwf.storeInSave) wpkAppend(pwf.fname, false); // non-system pak
2974 AddAnyFile(pwf.fname, true);
2977 // ignore cosmetic pwads
2978 if (!pwf.storeInSave) {
2979 for (int f = currFCount; f < fsysSearchPaths.length(); ++f) fsysSearchPaths[f]->cosmetic = true;
2982 wpkAddMarked(mark);
2984 fsys_hasPwads = true;
2986 FL_EndUserWads(); // stop marking
2988 fsys_skipSounds = false;
2989 fsys_skipSprites = false;
2990 fsys_skipDehacked = false;
2991 } else {
2992 pwadsSaved.restore();
2993 for (auto &&it : wpklistSaved) wpklist.append(it);
2994 for (auto &&it : wpklistSavedSmall) wpklistSmall.append(it);
2997 // load custom mode pwads
2998 CustomModeLoadPwads(CM_POST_PWADS);
3000 fsys_report_added_paks = !!reportWads;
3002 if (!fsys_DisableBDW && cli_BDWMod > 0) AddGameDir("basev/mods/bdw");
3003 if (cli_LudoGibsMod != 0) AddGameDir("basev/mods/ludus_gibs");
3005 if (cli_SkeeHUD > 0) mdetect_AddMod("skeehud");
3007 for (auto &&xmod : modAddMods) {
3008 GCon->Logf(NAME_Init, "adding special built-in mod '%s'", *xmod);
3009 AddGameDir(va("basev/mods/%s", *xmod));
3011 modAddMods.clear(); // don't need it anymore
3013 fsys_report_added_paks = !!reportWads;
3015 RenameSprites();
3017 if (doStartMap && fsys_warp_cmd.isEmpty() && !mapinfoFound && mapnum > 0 && mapname.length()) {
3018 GCon->Logf(NAME_Init, "FOUND MAP: %s", *mapname);
3019 mapname = va("map \"%s\"\n", *mapname.quote());
3020 //GCmdBuf.Insert(mapname);
3021 fsys_warp_cmd = mapname;
3022 } else if (doStartMap && fsys_warp_cmd.isEmpty() && mapinfoFound) {
3023 fsys_warp_cmd = "__k8_run_first_map";
3026 // look for "+map"
3027 if (fsys_warp_cmd.length() == 0) {
3028 if (GArgs.CheckParm("+map") != 0) Host_CLIMapStartFound();
3031 FreeDetectors(); // we don't need them
3035 //==========================================================================
3037 // FL_Shutdown
3039 //==========================================================================
3040 void FL_Shutdown () {
3041 fl_basedir.Clean();
3042 fl_savedir.Clean();
3043 fl_gamedir.Clean();
3044 fl_configdir.Clean();
3045 IWadDirs.Clear();
3046 FSYS_Shutdown();
3050 //==========================================================================
3052 // FL_OpenFileWrite
3054 //==========================================================================
3055 VStream *FL_OpenFileWrite (VStr Name, bool isFullName) {
3056 VStr tmpName;
3057 if (isFullName) {
3058 tmpName = Name;
3059 } else {
3060 if (fl_savedir.IsNotEmpty()) {
3061 tmpName = fl_savedir+"/"+fl_gamedir+"/"+Name;
3062 } else {
3063 tmpName = fl_basedir+"/"+fl_gamedir+"/"+Name;
3066 return FL_OpenSysFileWrite(tmpName);
3070 //==========================================================================
3072 // FL_OpenFileReadInCfgDir
3074 //==========================================================================
3075 VStream *FL_OpenFileReadInCfgDir (VStr Name) {
3076 if (Name.isEmpty()) return nullptr;
3077 VStr diskName = FL_GetConfigDir().appendPath(Name);
3078 VStream *strm = FL_OpenSysFileRead(diskName);
3079 if (strm) return strm;
3080 return FL_OpenFileRead(Name);
3084 //==========================================================================
3086 // FL_OpenFileWriteInCfgDir
3088 //==========================================================================
3089 VStream *FL_OpenFileWriteInCfgDir (VStr Name) {
3090 if (Name.isEmpty()) return nullptr;
3091 VStr diskName = FL_GetConfigDir().appendPath(Name);
3092 return FL_OpenSysFileWrite(diskName);
3096 //==========================================================================
3098 // FL_GetConfigDir
3100 //==========================================================================
3101 VStr FL_GetConfigDir () {
3102 VStr res = fl_configdir;
3103 if (res.isEmpty()) res = fl_savedir;
3104 if (res.isEmpty()) {
3105 #if !defined(_WIN32)
3106 const char *HomeDir = getenv("HOME");
3107 res = (HomeDir && HomeDir[0] ? VStr(HomeDir)+"/.k8vavoom" : VStr("."));
3108 #else
3109 //res = ".";
3110 res = VStr(VArgs::GetBinaryDir());
3111 #endif
3113 Sys_CreateDirectory(res);
3114 return res;
3118 //==========================================================================
3120 // FL_GetMapCacheDir
3122 //==========================================================================
3123 VStr FL_GetMapCacheDir () {
3124 VStr res = FL_GetConfigDir();
3125 if (res.isEmpty()) return res;
3126 res += "/.mapcache";
3127 Sys_CreateDirectory(res);
3128 return res;
3132 //==========================================================================
3134 // FL_GetVoxelCacheDir
3136 //==========================================================================
3137 VStr FL_GetVoxelCacheDir () {
3138 VStr res = FL_GetConfigDir();
3139 if (res.isEmpty()) return res;
3140 res += "/.voxcache";
3141 Sys_CreateDirectory(res);
3142 return res;
3146 //==========================================================================
3148 // FL_GetSavesDir
3150 //==========================================================================
3151 VStr FL_GetSavesDir () {
3152 VStr res = fl_savedir;
3153 if (res.isEmpty()) {
3154 #if !defined(_WIN32)
3155 const char *HomeDir = getenv("HOME");
3156 res = (HomeDir && HomeDir[0] ? VStr(HomeDir)+"/.k8vavoom/saves" : VStr("./saves"));
3157 #else
3158 res = "./saves";
3159 #endif
3161 Sys_CreateDirectory(res);
3162 return res;
3166 //==========================================================================
3168 // FL_GetScreenshotsDir
3170 //==========================================================================
3171 VStr FL_GetScreenshotsDir () {
3172 VStr res = FL_GetConfigDir();
3173 if (res.isEmpty()) return res;
3174 res += "/sshots";
3175 Sys_CreateDirectory(res);
3176 return res;
3180 //==========================================================================
3182 // FL_GetUserDataDir
3184 //==========================================================================
3185 VStr FL_GetUserDataDir (bool shouldCreate) {
3186 VStr res = FL_GetConfigDir();
3187 res += "/userdata";
3188 //res += '/'; res += game_name;
3189 if (shouldCreate) Sys_CreateDirectory(res);
3190 return res;
3194 //==========================================================================
3196 // FL_GetNetWadsCount
3198 //==========================================================================
3199 int FL_GetNetWadsCount () {
3200 int res = 0;
3201 for (auto &&sp : fsysSearchPaths) {
3202 if (!getNetPath(sp).isEmpty()) ++res;
3204 return res;
3208 //==========================================================================
3210 // FL_GetNetWadsHash
3212 //==========================================================================
3213 vuint32 FL_GetNetWadsHash () {
3214 int count = 0;
3215 VStr modlist;
3216 for (auto &&sp : fsysSearchPaths) {
3217 VStr s = getNetPath(sp);
3218 if (s.isEmpty()) continue;
3219 modlist += s;
3220 modlist += "\n";
3221 ++count;
3223 return XXH32(*modlist, (vint32)modlist.length(), (vuint32)count);
3227 //==========================================================================
3229 // FL_GetNetWads
3231 //==========================================================================
3232 void FL_GetNetWads (TArray<VStr> &list) {
3233 list.reset();
3234 for (auto &&sp : fsysSearchPaths) {
3235 VStr s = getNetPath(sp);
3236 if (s.isEmpty()) continue;
3237 //GCon->Logf(NAME_Debug, ":: <%s> (basepak=%d; iwad=%d; userwad=%d)", *s, (int)sp->basepak, (int)sp->iwad, (int)sp->userwad);
3238 list.append(s);
3243 //==========================================================================
3245 // FL_BuildRequiredWads
3247 //==========================================================================
3248 void FL_BuildRequiredWads () {
3249 #if 0
3250 // scan all textures, and mark any archive with textures as "required"
3251 GCon->Logf(NAME_Debug, "scanning %d textures", GTextureManager.GetNumTextures());
3252 for (int f = 0; f < GTextureManager.GetNumTextures(); ++f) {
3253 VTexture *tex = GTextureManager[f];
3254 if (tex && tex->SourceLump >= 0) {
3255 int fl = W_LumpFile(tex->SourceLump);
3256 if (fl >= 0 && fl < fsysSearchPaths.length()) fsysSearchPaths[fl]->required = true;
3260 // scan all archives, and mark any archives with important files as "required"
3261 GCon->Log(NAME_Debug, "scanning archives");
3262 for (auto &&arc : fsysSearchPaths) {
3263 if (arc->required) continue; // already marked
3264 for (int fn = arc->IterateNS(0, WADNS_Global); fn >= 0; fn = arc->IterateNS(fn+1, WADNS_Global)) {
3265 //GCon->Logf(NAME_Debug, " %d: %s : %s", fn, *arc->GetPrefix(), *arc->LumpName(fn));
3266 if (arc->LumpName(fn) == "decorate" || arc->LumpName(fn) == "dehacked") {
3267 arc->required = true;
3268 break;
3273 for (auto &&arc : fsysSearchPaths) if (arc->required) GCon->Logf(NAME_Debug, "rq: <%s>", *arc->GetPrefix());
3274 #endif