script, ui: added "sv_min_startmap_health" cvar
[k8vavoom.git] / source / cmd.cpp
blob9c6be8ae42c14c91347b6c6cb9d810b4f8cd1cd1
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 "psim/p_player.h"
28 #include "server/server.h"
29 #include "mapinfo.h"
30 #include "host.h"
31 #include "filesys/files.h"
32 #include "sound/sound.h"
33 #ifdef CLIENT
34 # include "text.h"
35 # include "client/client.h"
36 #endif
38 extern VCvarB sv_cheats;
40 VCmdBuf GCmdBuf;
42 bool VCommand::execLogInit = true;
44 bool VCommand::ParsingKeyConf;
46 bool VCommand::Initialised = false;
47 VStr VCommand::Original;
49 bool VCommand::rebuildCache = true;
50 TMap<VStrCI, VCommand *> VCommand::locaseCache;
52 TArray<VStr> VCommand::Args;
53 VCommand::ECmdSource VCommand::Source;
54 VBasePlayer *VCommand::Player;
56 TArray<VStr> VCommand::AutoCompleteTable;
57 static TMap<VStr, bool> AutoCompleteTableBSet; // quicksearch
59 VCommand *VCommand::Cmds = nullptr;
60 //VCommand::VAlias *VCommand::Alias = nullptr;
61 TArray<VCommand::VAlias> VCommand::AliasList;
62 TMap<VStrCI, int> VCommand::AliasMap;
64 bool VCommand::cliInserted = false;
65 VStr VCommand::cliPreCmds;
67 void (*VCommand::onShowCompletionMatch) (bool isheader, VStr s);
69 VStr VCommand::CurrKeyConfKeySection;
70 VStr VCommand::CurrKeyConfWeaponSection;
72 static const char *KeyConfCommands[] = {
73 "alias",
74 "bind",
75 "defaultbind",
76 "addkeysection",
77 "addmenukey",
78 "addslotdefault",
79 "weaponsection",
80 "setslot",
81 "addplayerclass",
82 "clearplayerclasses"
85 static bool wasRunCliCommands = false;
88 //==========================================================================
90 // sortCmpVStrCI
92 //==========================================================================
93 static int sortCmpVStrCI (const void *a, const void *b, void * /*udata*/) {
94 if (a == b) return 0;
95 VStr *sa = (VStr *)a;
96 VStr *sb = (VStr *)b;
97 return sa->ICmp(*sb);
101 //==========================================================================
103 // isBoolTrueStr
105 //==========================================================================
106 static bool isBoolTrueStr (VStr s) {
107 if (s.isEmpty()) return false;
109 s = s.xstrip();
110 if (s.isEmpty()) return false;
111 if (s.strEquCI("false")) return false;
112 if (s.strEquCI("ona")) return false;
113 if (s.strEquCI("0")) return false;
114 if (s.indexOf('.') >= 0) {
115 float f = -1.0f;
116 if (s.convertFloat(&f)) return (f != 0.0f);
117 } else {
118 int n = -1;
119 if (s.convertInt(&n)) return (n != 0);
121 return true;
123 return VCvar::ParseBool(*s);
127 //**************************************************************************
129 // Commands, alias
131 //**************************************************************************
133 //==========================================================================
135 // CheatAllowed
137 //==========================================================================
138 static bool CheatAllowed (VBasePlayer *Player, bool allowDead=false) {
139 if (!Player) return false;
140 if (sv.intermission) {
141 Player->Printf("You are not in game!");
142 return false;
144 // client will forward them anyway
145 if (GGameInfo->NetMode == NM_Client) return true;
146 if (GGameInfo->NetMode == NM_ListenServer || GGameInfo->NetMode == NM_DedicatedServer) {
147 if (!sv_cheats) {
148 Player->Printf("You cannot cheat in a network game!");
149 return false;
151 return true;
153 // if not a network server, cheats are always allowed
154 //return (GGameInfo->NetMode >= NM_Standalone);
156 if (GGameInfo->NetMode >= NM_DedicatedServer) {
157 Player->Printf("You cannot cheat in a network game!");
158 return false;
160 if (GGameInfo->WorldInfo->Flags&VWorldInfo::WIF_SkillDisableCheats) {
161 Player->Printf("You are too good to cheat!");
162 //k8: meh, if i want to cheat, i want to cheat!
163 //return false;
164 return true;
167 if (!allowDead && Player->Health <= 0) {
168 // dead players can't cheat
169 Player->Printf("You must be alive to cheat");
170 return false;
172 return true;
176 //==========================================================================
178 // VCommand::VCommand
180 //==========================================================================
181 VCommand::VCommand (const char *name) {
182 Next = Cmds;
183 Name = name;
184 Cmds = this;
185 if (Initialised) AddToAutoComplete(Name);
189 //==========================================================================
191 // VCommand::~VCommand
193 //==========================================================================
194 VCommand::~VCommand () {
198 //==========================================================================
200 // VCommand::AutoCompleteArg
202 // return non-empty string to replace arg
204 //==========================================================================
205 VStr VCommand::AutoCompleteArg (const TArray<VStr> &/*args*/, int /*aidx*/) {
206 return VStr::EmptyString;
210 //==========================================================================
212 // VCommand::Init
214 //==========================================================================
215 void VCommand::Init () {
216 for (VCommand *cmd = Cmds; cmd; cmd = cmd->Next) AddToAutoComplete(cmd->Name);
218 // add configuration file execution
219 GCmdBuf.Insert("exec k8vavoom_startup.vs\n__run_cli_commands__\n");
221 Initialised = true;
225 //==========================================================================
227 // VCommand::InsertCLICommands
229 //==========================================================================
230 void VCommand::InsertCLICommands () {
231 VStr cstr;
233 if (!cliPreCmds.isEmpty()) {
234 cstr = cliPreCmds;
235 if (!cstr.endsWith("\n")) cstr += '\n';
236 cliPreCmds.clear();
239 // add console commands from command line
240 // these are params, that start with + and continue until the end or until next param that starts with - or +
241 bool in_cmd = false;
242 for (int i = 1; i < GArgs.Count(); ++i) {
243 if (in_cmd) {
244 // check for number
245 if (GArgs[i] && (GArgs[i][0] == '-' || GArgs[i][0] == '+')) {
246 float v;
247 if (VStr::convertFloat(GArgs[i], &v)) {
248 cstr += ' ';
249 cstr += '"';
250 cstr += VStr(GArgs[i]).quote();
251 cstr += '"';
252 continue;
255 if (!GArgs[i] || GArgs[i][0] == '-' || GArgs[i][0] == '+') {
256 in_cmd = false;
257 //GCmdBuf << "\n";
258 cstr += '\n';
259 } else {
260 //GCmdBuf << " \"" << VStr(GArgs[i]).quote() << "\"";
261 cstr += ' ';
262 cstr += '"';
263 cstr += VStr(GArgs[i]).quote();
264 cstr += '"';
265 continue;
268 if (GArgs[i][0] == '+') {
269 in_cmd = true;
270 //GCmdBuf << (GArgs[i]+1);
271 cstr += (GArgs[i]+1);
274 //if (in_cmd) GCmdBuf << "\n";
275 if (in_cmd) cstr += '\n';
277 //GCon->Logf("===\n%s\n===", *cstr);
279 if (!cstr.isEmpty()) GCmdBuf.Insert(cstr);
283 // ////////////////////////////////////////////////////////////////////////// //
284 static int vapcmp (const void *aa, const void *bb, void * /*udata*/) {
285 const VCommand::VAlias *a = *(const VCommand::VAlias **)aa;
286 const VCommand::VAlias *b = *(const VCommand::VAlias **)bb;
287 if (a == b) return 0;
288 return a->Name.ICmp(b->Name);
291 static int vstrptrcmpci (const void *aa, const void *bb, void * /*udata*/) {
292 const VStr *a = (const VStr *)aa;
293 const VStr *b = (const VStr *)bb;
294 if (a == b) return 0;
295 return a->ICmp(*b);
299 //==========================================================================
301 // VCommand::WriteAlias
303 //==========================================================================
304 void VCommand::WriteAlias (VStream *st) {
305 // build list
306 TArray<VAlias *> alist;
307 for (auto &&al : AliasList) if (al.Save) alist.append(&al);
308 if (alist.length() == 0) return;
309 // sort list
310 smsort_r(alist.ptr(), alist.length(), sizeof(VAlias *), &vapcmp, nullptr);
311 // write list
312 for (auto &&al : alist) {
313 st->writef("alias %s \"%s\"\n", *al->Name, *al->CmdLine.quote());
318 //==========================================================================
320 // VCommand::Shutdown
322 //==========================================================================
323 void VCommand::Shutdown () {
324 AliasMap.clear();
325 AliasList.clear();
326 AutoCompleteTable.Clear();
327 AutoCompleteTableBSet.clear();
328 Args.Clear();
329 Original.Clean();
330 locaseCache.clear();
334 //==========================================================================
336 // VCommand::LoadKeyconfLump
338 //==========================================================================
339 void VCommand::LoadKeyconfLump (int Lump) {
340 if (Lump < 0) return;
341 // read it
342 VStream *Strm = W_CreateLumpReaderNum(Lump);
343 if (!Strm) return;
345 VStr buf;
346 buf.setLength(Strm->TotalSize(), 0);
347 Strm->Serialize(buf.getMutableCStr(), buf.length());
348 if (Strm->IsError()) buf.clear();
349 VStream::Destroy(Strm);
351 GCon->Logf(NAME_Init, "loading keyconf from '%s'...", *W_FullLumpName(Lump));
353 // parse it
354 VCmdBuf CmdBuf;
355 TArray<VStr> lines;
356 TArray<VStr> args;
357 buf.split('\n', lines);
358 for (auto &&s : lines) {
359 s = s.xstrip();
360 if (s.length() == 0 || s[0] == '#' || s[0] == '/') continue;
361 args.reset();
362 s.tokenise(args);
363 if (args.length() == 0) continue;
365 if (args[0].strEquCI("defaultbind")) {
366 GCon->Logf(NAME_Warning, "ignored keyconf command: %s", *s);
367 } else
370 CmdBuf << s << "\n";
374 // enable special mode for console commands
375 ParsingKeyConf = true;
376 CmdBuf.Exec();
377 // back to normal console command execution
378 ParsingKeyConf = false;
382 //==========================================================================
384 // VCommand::ProcessKeyConf
386 //==========================================================================
388 void VCommand::ProcessKeyConf () {
389 for (auto &&it : WadNSNameIterator(NAME_keyconf, WADNS_Global)) {
390 const int Lump = it.lump;
391 LoadKeyconfLump(Lump);
397 //==========================================================================
399 // VCommand::AddToAutoComplete
401 //==========================================================================
402 void VCommand::AddToAutoComplete (const char *string) {
403 if (!string || !string[0] || string[0] == '_' || (vuint8)string[0] < 32 || (vuint8)string[0] >= 127) return;
405 VStr vs(string);
406 VStr vslow = vs.toLowerCase();
408 if (AutoCompleteTableBSet.has(vslow)) return;
410 AutoCompleteTableBSet.put(vslow, true);
411 AutoCompleteTable.Append(vs);
413 // alphabetic sort
414 for (int i = AutoCompleteTable.length()-1; i && AutoCompleteTable[i-1].ICmp(AutoCompleteTable[i]) > 0; --i) {
415 VStr swap = AutoCompleteTable[i];
416 AutoCompleteTable[i] = AutoCompleteTable[i-1];
417 AutoCompleteTable[i-1] = swap;
422 //==========================================================================
424 // VCommand::RemoveFromAutoComplete
426 //==========================================================================
427 void VCommand::RemoveFromAutoComplete (const char *string) {
428 if (!string || !string[0] || string[0] == '_') return;
430 VStr vs(string);
431 VStr vslow = vs.toLowerCase();
433 if (!AutoCompleteTableBSet.has(vslow)) return; // nothing to do
435 AutoCompleteTableBSet.del(vslow);
436 for (int f = 0; f < AutoCompleteTable.length(); ++f) {
437 if (AutoCompleteTable[f].strEquCI(vs)) {
438 AutoCompleteTable.removeAt(f);
439 --f;
445 //==========================================================================
447 // acPartialQuote
449 //==========================================================================
450 static VStr acPartialQuote (VStr s, bool doQuoting, bool hasSpacedMatch=false) {
451 if (!doQuoting) return s;
452 if (!hasSpacedMatch && !s.needQuoting()) return s;
453 //return VStr("\"")+s.quote();
454 // close quote, autocompleter can cope with that
455 if (hasSpacedMatch && !s.needQuoting()) return VStr("\"")+s+"\"";
456 return s.quote(true);
460 //==========================================================================
462 // VCommand::AutoCompleteFromList
464 //==========================================================================
465 VStr VCommand::AutoCompleteFromList (VStr prefix, const TArray <VStr> &list, bool unchangedAsEmpty, bool doSortHint, bool doQuoting) {
466 if (list.length() == 0) return (unchangedAsEmpty ? VStr::EmptyString : acPartialQuote(prefix, doQuoting));
468 VStr bestmatch;
469 int matchcount = 0;
471 // first, get longest match
472 for (auto &&mt : list) {
473 if (mt.length() < prefix.length()) continue;
474 if (VStr::NICmp(*prefix, *mt, prefix.length()) != 0) continue;
475 ++matchcount;
476 if (bestmatch.length() < mt.length()) bestmatch = mt;
479 if (matchcount == 0) return (unchangedAsEmpty ? VStr::EmptyString : acPartialQuote(prefix, doQuoting)); // alas
480 if (matchcount == 1) { if (doQuoting) bestmatch = bestmatch.quote(true); bestmatch += " "; return bestmatch; } // done
482 // trim match; check for spaces too
483 bool hasSpacedMatch = false;
484 for (auto &&mt : list) {
485 if (mt.length() < prefix.length()) continue;
486 if (VStr::NICmp(*prefix, *mt, prefix.Length()) != 0) continue;
487 // cannot be longer than this
488 if (!hasSpacedMatch && (mt.indexOf(' ') >= 0 || mt.indexOf('\t') >= 0)) hasSpacedMatch = true;
489 if (bestmatch.length() > mt.length()) bestmatch = bestmatch.left(mt.length());
490 int mlpos = 0;
491 while (mlpos < bestmatch.length()) {
492 if (VStr::upcase1251(bestmatch[mlpos]) != VStr::upcase1251(mt[mlpos])) {
493 bestmatch = bestmatch.left(mlpos);
494 break;
496 ++mlpos;
501 GCon->Logf("prefix: <%s>", *prefix);
502 GCon->Logf("bestmatch: <%s>", *bestmatch);
503 GCon->Logf("hasSpacedMatch: %d", (int)hasSpacedMatch);
506 // if match equals to prefix, this is second tab tap, so show all possible matches
507 if (bestmatch == prefix) {
508 // show all possible matches
509 if (onShowCompletionMatch) {
510 onShowCompletionMatch(true, "=== possible matches ===");
511 bool skipPrint = false;
512 if (doSortHint && list.length() > 1) {
513 bool needSorting = false;
514 for (int f = 1; f < list.length(); ++f) if (list[f-1].ICmp(list[f]) > 0) { needSorting = true; break; }
515 if (needSorting) {
516 TArray<VStr> sortedlist;
517 sortedlist.resize(list.length());
518 for (int f = 0; f < list.length(); ++f) sortedlist.append(list[f]);
519 smsort_r(sortedlist.ptr(), sortedlist.length(), sizeof(VStr), &vstrptrcmpci, nullptr);
520 for (int f = 0; f < sortedlist.length(); ++f) {
521 VStr mt = sortedlist[f];
522 if (mt.length() < prefix.length()) continue;
523 if (VStr::NICmp(*prefix, *mt, prefix.Length()) != 0) continue;
524 onShowCompletionMatch(false, mt);
526 skipPrint = true;
529 if (!skipPrint) {
530 for (int f = 0; f < list.length(); ++f) {
531 VStr mt = list[f];
532 if (mt.length() < prefix.length()) continue;
533 if (VStr::NICmp(*prefix, *mt, prefix.Length()) != 0) continue;
534 onShowCompletionMatch(false, mt);
538 return (unchangedAsEmpty ? VStr::EmptyString : acPartialQuote(prefix, doQuoting, hasSpacedMatch));
541 // found extended match
542 return acPartialQuote(bestmatch, doQuoting, hasSpacedMatch);
546 //==========================================================================
548 // findPlayer
550 //==========================================================================
551 static VBasePlayer *findPlayer () {
552 if (sv.intermission) return nullptr;
553 if (GGameInfo->NetMode < NM_Standalone) return nullptr; // not playing
554 #ifdef CLIENT
555 if (GGameInfo->NetMode == NM_Client) return (cl && cl->Net ? cl : nullptr);
556 #endif
557 // find any active player
558 for (int f = 0; f < MAXPLAYERS; ++f) {
559 VBasePlayer *plr = GGameInfo->Players[f];
560 if (!plr) continue;
561 if ((plr->PlayerFlags&VBasePlayer::PF_IsBot) ||
562 !(plr->PlayerFlags&VBasePlayer::PF_Spawned))
564 continue;
566 if (plr->PlayerState != PST_LIVE || plr->Health <= 0) continue;
567 return plr;
569 return nullptr;
573 //==========================================================================
575 // VCommand::GetAutoComplete
577 // if returned string ends with space, this is the only match
579 //==========================================================================
580 VStr VCommand::GetAutoComplete (VStr prefix) {
581 if (prefix.length() == 0) return prefix; // oops
583 //GCon->Logf("000: PFX=<%s>", *prefix);
585 TArray<VStr> args;
586 prefix.tokenize(args);
587 int aidx = args.length();
588 if (aidx == 0) return prefix; // wtf?!
590 //GCon->Logf("001: len=%d; [$-1]=<%s>", args.length(), *args[args.length()-1]);
592 bool endsWithBlank = ((vuint8)prefix[prefix.length()-1] <= ' ');
594 if (aidx == 1 && !endsWithBlank) {
595 VBasePlayer *plr = findPlayer();
596 if (plr) {
597 auto otbllen = AutoCompleteTable.length();
598 plr->ListConCommands(AutoCompleteTable, prefix);
599 //GCon->Logf("***PLR: pfx=<%s>; found=%d", *prefix, AutoCompleteTable.length()-otbllen);
600 if (AutoCompleteTable.length() > otbllen) {
601 // copy and sort
602 TArray<VStr> newlist;
603 newlist.setLength(AutoCompleteTable.length());
604 for (int f = 0; f < AutoCompleteTable.length(); ++f) newlist[f] = AutoCompleteTable[f];
605 AutoCompleteTable.setLength<false>(otbllen); // don't resize
606 smsort_r(newlist.ptr(), newlist.length(), sizeof(VStr), &sortCmpVStrCI, nullptr);
607 return AutoCompleteFromList(prefix, newlist);
610 return AutoCompleteFromList(prefix, AutoCompleteTable);
613 // autocomplete new arg?
614 if (aidx > 1 && !endsWithBlank) --aidx; // nope, last arg
616 // check for command
617 rebuildCommandCache();
619 auto cptr = locaseCache.get(args[0]);
620 if (cptr) {
621 VCommand *cmd = *cptr;
622 VStr ac = cmd->AutoCompleteArg(args, aidx);
623 if (ac.length()) {
624 // autocompleted, rebuild string
625 //if (aidx < args.length()) args[aidx] = ac; else args.append(ac);
626 //!bool addSpace = ((vuint8)ac[ac.length()-1] <= ' ');
627 //!if (addSpace) ac.chopRight(1);
628 VStr res;
629 for (int f = 0; f < aidx; ++f) {
630 res += args[f].quote(true); // add quote chars if necessary
631 res += ' ';
634 if (ac.length()) {
635 ac = ac.quote(true);
636 if (!addSpace && ac[ac.length()-1] == '"') ac.chopRight(1);
638 res += ac;
639 if (addSpace) res += ' ';
641 res += ac;
642 return res;
644 // cannot complete, nothing's changed
645 return prefix;
648 // try player
650 VBasePlayer *plr = findPlayer();
651 if (plr) {
652 TArray<VStr> aclist;
653 if (plr->ExecConCommandAC(args, endsWithBlank, aclist)) {
654 if (aclist.length() == 0) return prefix; // nothing's found
655 // rebuild string
656 VStr res;
657 for (int f = 0; f < aidx; ++f) {
658 res += args[f].quote(true); // add quote chars if necessary
659 res += ' ';
661 // several matches
662 // sort
663 smsort_r(aclist.ptr(), aclist.length(), sizeof(VStr), &sortCmpVStrCI, nullptr);
664 //for (int f = 0; f < aclist.length(); ++f) GCon->Logf(" %d:<%s>", f, *aclist[f]);
665 VStr ac = AutoCompleteFromList((endsWithBlank ? VStr() : args[args.length()-1]), aclist, false, true, true);
667 bool addSpace = ((vuint8)ac[ac.length()-1] <= ' ');
668 if (addSpace) ac.chopRight(1);
669 if (ac.length()) {
670 ac = ac.quote(true);
671 if (!addSpace && ac[ac.length()-1] == '"') ac.chopRight(1);
673 if (ac.length()) {
674 res += ac;
675 if (addSpace) res += ' ';
678 res += ac;
679 return res;
684 // Cvar
685 if (aidx == 1) {
686 // show cvar help, why not?
687 if (onShowCompletionMatch) {
688 VCvar *var = VCvar::FindVariable(*args[0]);
689 if (var) {
690 VStr help = var->GetHelp();
691 if (help.length() && !VStr(help).startsWithNoCase("no help yet")) {
692 onShowCompletionMatch(false, args[0]+": "+help);
698 // nothing's found
699 return prefix;
703 //**************************************************************************
705 // Parsing of a command, command arg handling
707 //**************************************************************************
710 //==========================================================================
712 // VCommand::SubstituteArgs
714 // substiture "${1}" and such args; used for aliases
715 // use "${1q}" to insert properly quoted argument (WITHOUT quotation marks)
717 //==========================================================================
718 VStr VCommand::SubstituteArgs (VStr str) {
719 if (str.isEmpty()) return VStr::EmptyString;
720 if (str.indexOf('$') < 0) return str;
721 const char *s = *str;
722 VStr res;
723 while (*s) {
724 const char ch = *s++;
725 if (ch == '$' && (*s != '(' || *s == '{')) {
726 const char *sstart = s-1;
727 const char ech = (*s == '{' ? '}' : ')');
728 ++s;
729 if (!s[0]) break;
730 const char *se = s;
731 while (*se && *se != ech) ++se;
732 if (se != s) {
733 VStr cvn(s, (int)(ptrdiff_t)(se-s));
734 cvn = cvn.xstrip();
735 if (cvn.length() && cvn[0] >= '0' && cvn[0] <= '9') {
736 int n = -1;
737 bool quoted = false;
738 if (cvn.length() > 1 && cvn[cvn.length()-1] == 'q') {
739 quoted = true;
740 cvn.chopRight(1);
742 if (cvn.convertInt(&n)) {
743 if (n >= 0 && n < Args.length()) { // overflow protection
744 if (quoted) res += Args[n].quote(); else res += Args[n];
746 s = se+(*se != 0);
747 continue;
751 if (*se) ++se;
752 res += VStr(sstart, (int)(ptrdiff_t)(se-sstart));
753 s = se;
754 } else {
755 res += ch;
756 if (ch == '\\' && *s) res += *s++;
759 return res;
763 //==========================================================================
765 // VCommand::ExpandSigil
767 //==========================================================================
768 VStr VCommand::ExpandSigil (VStr str) {
769 if (str.isEmpty()) return VStr::EmptyString;
770 if (str.indexOf('$') < 0) return str;
771 const char *s = *str;
772 VStr res;
773 while (*s) {
774 const char ch = *s++;
775 if (ch == '$' && (*s != '(' || *s == '{')) {
776 const char ech = (*s == '{' ? '}' : ')');
777 ++s;
778 if (!s[0]) break;
779 const char *se = s;
780 while (*se && *se != ech) ++se;
781 if (se != s) {
782 VStr cvn(s, (int)(ptrdiff_t)(se-s));
783 cvn = cvn.xstrip();
784 VCvar *cv = VCvar::FindVariable(*cvn);
785 if (cv) res += cv->asStr();
787 s = se+(*se != 0);
788 } else {
789 res += ch;
790 if (ch == '\\' && *s) res += *s++;
793 return res;
797 //==========================================================================
799 // VCommand::TokeniseString
801 //==========================================================================
802 void VCommand::TokeniseString (VStr str) {
803 Original = str;
804 Args.reset();
805 str.tokenize(Args);
806 // expand sigils (except the command name)
807 //HACK: for "alias" command, don't perform any expanding
808 if (!Args[0].strEquCI("alias")) {
809 for (int f = 1; f < Args.length(); ++f) Args[f] = ExpandSigil(Args[f]);
814 //==========================================================================
816 // VCommand::rebuildCommandCache
818 //==========================================================================
819 void VCommand::rebuildCommandCache () {
820 if (!rebuildCache) return;
821 rebuildCache = false;
822 locaseCache.clear();
823 for (VCommand *cmd = Cmds; cmd; cmd = cmd->Next) {
824 locaseCache.put(cmd->Name, cmd);
829 //==========================================================================
831 // VCommand::GetCommandType
833 //==========================================================================
834 int VCommand::GetCommandType (VStr cmd) {
835 if (cmd.isEmpty()) return CT_UNKNOWN;
837 rebuildCommandCache();
839 auto cptr = locaseCache.get(cmd);
840 if (cptr) return CT_COMMAND;
842 VBasePlayer *plr = findPlayer();
843 if (plr && plr->IsConCommand(cmd)) return CT_COMMAND;
845 if (VCvar::HasVar(*cmd)) return CT_CVAR;
847 auto idp = AliasMap.get(cmd);
848 if (idp) return CT_ALIAS;
850 return CT_UNKNOWN;
854 //==========================================================================
856 // VCommand::ProcessSetCommand
858 // "set [(type)] var value" (i.e. "set (int) myvar 0")
859 // (type) is:
860 // (int)
861 // (float)
862 // (bool)
863 // (string)
864 // default is "(string)"
866 //==========================================================================
867 void VCommand::ProcessSetCommand () {
868 enum {
869 SST_Int,
870 SST_Float,
871 SST_Bool,
872 SST_Str,
875 if (Args.length() < 3 || Args.length() > 4) {
876 if (host_initialised) GCon->Log(NAME_Error, "'set' command usage: \"set [(type)] name value\"");
877 return;
880 int stype = SST_Str;
881 int nidx = 1;
883 if (Args.length() == 4) {
884 nidx = 2;
885 if (Args[1].strEquCI("(int)")) stype = SST_Int;
886 else if (Args[1].strEquCI("(float)")) stype = SST_Float;
887 else if (Args[1].strEquCI("(bool)")) stype = SST_Bool;
888 else if (Args[1].strEquCI("(string)")) stype = SST_Str;
889 else if (Args[1].strEquCI("(str)")) stype = SST_Str;
890 else {
891 if (host_initialised) GCon->Logf(NAME_Error, "invalid variable type `%s` in 'set'", *Args[1]);
892 return;
896 VStr vname = Args[nidx].xstrip();
897 VStr vvalue = Args[nidx+1];
899 if (vname.isEmpty()) {
900 if (host_initialised) GCon->Log(NAME_Error, "'set' expects variable name");
901 return;
904 float fv = 0.0f;
905 int iv = 0;
906 bool bv = false;
908 switch (stype) {
909 case SST_Int:
910 if (!vvalue.convertInt(&iv)) {
911 if (host_initialised) GCon->Logf(NAME_Error, "'set' expects int, but got `%s`", *vvalue);
912 return;
914 break;
915 case SST_Float:
916 if (!vvalue.convertFloat(&fv)) {
917 if (host_initialised) GCon->Logf(NAME_Error, "'set' expects float, but got `%s`", *vvalue);
918 return;
920 break;
921 case SST_Bool:
922 bv = isBoolTrueStr(vvalue);
923 break;
924 case SST_Str:
925 break;
926 default: Sys_Error("VCommand::ProcessSetCommand: wtf vconvert?!");
929 VCvar *cv = VCvar::FindVariable(*vname);
930 if (!cv) {
931 switch (stype) {
932 case SST_Int: cv = VCvar::CreateNewInt(VName(*vname), 0, "user-created variable", CVAR_User); break;
933 case SST_Float: cv = VCvar::CreateNewFloat(VName(*vname), 0.0f, "user-created variable", CVAR_User); break;
934 case SST_Bool: cv = VCvar::CreateNewBool(VName(*vname), false, "user-created variable", CVAR_User); break;
935 case SST_Str: cv = VCvar::CreateNewStr(VName(*vname), "", "user-created variable", CVAR_User); break;
936 default: Sys_Error("VCommand::ProcessSetCommand: wtf vcreate?!");
938 } else {
939 if (cv->IsReadOnly()) {
940 if (host_initialised) GCon->Logf(NAME_Error, "'set' tried to modify read-only cvar '%s'", *vname);
941 return;
944 vassert(cv);
946 switch (stype) {
947 case SST_Int: cv->SetInt(iv); break;
948 case SST_Float: cv->SetFloat(fv); break;
949 case SST_Bool: cv->SetBool(bv); break;
950 case SST_Str: cv->SetStr(vvalue); break;
951 default: Sys_Error("VCommand::ProcessSetCommand: wtf vset?!");
956 //==========================================================================
958 // VCommand::ExecuteString
960 //==========================================================================
961 void VCommand::ExecuteString (VStr Acmd, ECmdSource src, VBasePlayer *APlayer) {
962 //GCon->Logf(NAME_Debug, "+++ command BEFORE tokenizing: <%s>; plr=%s\n", *Acmd, (APlayer ? APlayer->GetClass()->GetName() : "<none>"));
964 TokeniseString(Acmd);
965 Source = src;
966 Player = APlayer;
968 //GCon->Logf(NAME_Debug, "+++ command argc=%d (<%s>)\n", Args.length(), *Acmd);
969 //for (int f = 0; f < Args.length(); ++f) GCon->Logf(NAME_Debug, " #%d: <%s>\n", f, *Args[f]);
971 if (Args.length() == 0) return;
973 if (Args[0] == "__run_cli_commands__") {
974 if (!wasRunCliCommands) {
975 wasRunCliCommands = true;
976 FL_ProcessPreInits(); // override configs
977 FL_ClearPreInits();
978 GSoundManager->InitThreads();
979 if (!cliInserted) {
980 #ifdef CLIENT
981 if (!R_IsDrawerInited()) {
982 wasRunCliCommands = false;
983 GCmdBuf.Insert("wait\n__run_cli_commands__\n");
984 return;
986 #endif
987 execLogInit = false;
988 cliInserted = true;
989 InsertCLICommands();
992 return;
995 VStr ccmd = Args[0];
997 // conditionals
998 if (ccmd.length() >= 3 && ccmd[0] == '$' && VStr::tolower(ccmd[1]) == 'i' && VStr::tolower(ccmd[2]) == 'f') {
999 if (ccmd.strEquCI("$if") || ccmd.strEquCI("$ifnot")) {
1000 if (Args.length() < 3) return;
1001 const bool neg = (ccmd.length() > 3);
1002 const bool bvtrue = (isBoolTrueStr(Args[1]) ? !neg : neg);
1003 if (!bvtrue) return;
1004 // remove condition command and expression
1005 Args.removeAt(0);
1006 Args.removeAt(0);
1007 } else if (ccmd.strEquCI("$if_or") || ccmd.strEquCI("$ifnot_or")) {
1008 if (Args.length() < 4) return;
1009 const bool neg = (ccmd.length() > 6);
1010 const bool bvtrue = (isBoolTrueStr(Args[1]) || isBoolTrueStr(Args[2]) ? !neg : neg);
1011 if (!bvtrue) return;
1012 // remove condition command and expressions
1013 Args.removeAt(0);
1014 Args.removeAt(0);
1015 Args.removeAt(0);
1016 } else if (ccmd.strEquCI("$if_and") || ccmd.strEquCI("$ifnot_and")) {
1017 if (Args.length() < 4) return;
1018 const bool neg = (ccmd.length() > 7);
1019 const bool bvtrue = (isBoolTrueStr(Args[1]) && isBoolTrueStr(Args[2]) ? !neg : neg);
1020 if (!bvtrue) return;
1021 // remove condition command and expressions
1022 Args.removeAt(0);
1023 Args.removeAt(0);
1024 Args.removeAt(0);
1026 ccmd = Args[0];
1027 if (ccmd.isEmpty()) return; // just in case
1030 if (ParsingKeyConf) {
1031 // verify that it's a valid keyconf command
1032 bool Found = false;
1033 for (unsigned i = 0; i < ARRAY_COUNT(KeyConfCommands); ++i) {
1034 if (ccmd.strEquCI(KeyConfCommands[i])) {
1035 Found = true;
1036 break;
1039 if (!Found) {
1040 GCon->Logf(NAME_Warning, "Invalid KeyConf command: %s", *Acmd);
1041 return;
1045 if (ccmd.strEquCI("set")) {
1046 ProcessSetCommand();
1047 return;
1050 // check for command
1051 rebuildCommandCache();
1053 auto cptr = locaseCache.get(ccmd);
1054 if (cptr) {
1055 if (cptr && !Player) Player = findPlayer(); // for local commands
1056 (*cptr)->Run();
1057 return;
1060 // check for player command
1061 if (Source == SRC_Command) {
1062 VBasePlayer *plr = findPlayer();
1063 if (plr && plr->IsConCommand(ccmd)) {
1064 ForwardToServer();
1065 return;
1067 } else if (Player && Player->IsConCommand(ccmd)) {
1068 if (CheatAllowed(Player)) Player->ExecConCommand();
1069 return;
1072 // Cvar
1073 if (FL_HasPreInit(ccmd)) return;
1075 // this hack allows setting cheating variables from command line or autoexec
1077 bool oldCheating = VCvar::GetCheating();
1078 bool cheatingChanged = false;
1079 VBasePlayer *plr = findPlayer();
1080 #ifdef CLIENT
1081 if (!plr || !cl || !cl->Net)
1082 #else
1083 if (!plr)
1084 #endif
1086 cheatingChanged = true;
1087 VCvar::SetCheating(/*VCvar::GetBool("sv_cheats")*/true);
1088 //GCon->Log(NAME_Debug, "forced: cheating and unlatching");
1090 //GCon->Logf("sv_cheats: %d; plr is %shere", (int)VCvar::GetBool("sv_cheats"), (plr ? "" : "not "));
1091 bool doneCvar = VCvar::Command(Args);
1092 if (cheatingChanged) VCvar::SetCheating(oldCheating);
1093 if (doneCvar) {
1094 if (cheatingChanged) VCvar::Unlatch();
1095 return;
1099 // check for command defined with ALIAS
1100 if (!ccmd.isEmpty()) {
1101 auto idp = AliasMap.get(ccmd);
1102 if (idp) {
1103 VAlias &al = AliasList[*idp];
1104 GCmdBuf.Insert("\n");
1105 GCmdBuf.Insert(SubstituteArgs(al.CmdLine));
1106 return;
1110 // unknown command
1111 #ifndef CLIENT
1112 if (host_initialised)
1113 #endif
1114 GCon->Logf(NAME_Error, "Unknown command '%s'", *ccmd);
1118 //==========================================================================
1120 // VCommand::ForwardToServer
1122 //==========================================================================
1123 void VCommand::ForwardToServer () {
1124 #ifdef CLIENT
1125 if (!cl) {
1126 GCon->Log("You must be in a game to execute this command");
1127 return;
1129 if (!CL_SendCommandToServer(Original)) {
1130 VCommand::ExecuteString(Original, VCommand::SRC_Client, cl);
1132 #else
1133 // for dedicated server, just execute it
1134 // yeah, it is marked "VCommand::SRC_Client", but this is just a bad name
1135 // actually, forwardig code only checks for `SRC_Command`, and forwards here
1136 VCommand::ExecuteString(Original, VCommand::SRC_Client, nullptr);
1137 #endif
1141 //==========================================================================
1143 // VCommand::CheckParm
1145 //==========================================================================
1146 int VCommand::CheckParm (const char *check) {
1147 if (check && check[0]) {
1148 for (int i = 1; i < Args.length(); ++i) {
1149 if (Args[i].strEquCI(check)) return i;
1152 return 0;
1156 //==========================================================================
1158 // VCommand::GetArgC
1160 //==========================================================================
1161 int VCommand::GetArgC () {
1162 return Args.length();
1166 //==========================================================================
1168 // VCommand::GetArgV
1170 //==========================================================================
1171 VStr VCommand::GetArgV (int idx) {
1172 if (idx < 0 || idx >= Args.length()) return VStr();
1173 return Args[idx];
1177 //**************************************************************************
1179 // Command buffer
1181 //**************************************************************************
1183 //==========================================================================
1185 // VCmdBuf::Insert
1187 //==========================================================================
1188 void VCmdBuf::Insert (const char *text) {
1189 if (text && text[0]) Buffer = VStr(text)+Buffer;
1193 //==========================================================================
1195 // VCmdBuf::Insert
1197 //==========================================================================
1198 void VCmdBuf::Insert (VStr text) {
1199 if (text.length()) Buffer = text+Buffer;
1203 //==========================================================================
1205 // VCmdBuf::Print
1207 //==========================================================================
1208 void VCmdBuf::Print (const char *data) {
1209 if (data && data[0]) Buffer += data;
1213 //==========================================================================
1215 // VCmdBuf::Print
1217 //==========================================================================
1218 void VCmdBuf::Print (VStr data) {
1219 if (data.length()) Buffer += data;
1223 //==========================================================================
1225 // VCmdBuf::checkWait
1227 // returns `false` (and resets `Wait`) if wait expired
1229 //==========================================================================
1230 bool VCmdBuf::checkWait () {
1231 if (!WaitEndTime) return false;
1232 const double currTime = Sys_Time();
1233 if (currTime >= WaitEndTime) {
1234 WaitEndTime = 0;
1235 return false;
1237 return true;
1241 //==========================================================================
1243 // VCmdBuf::Exec
1245 //==========================================================================
1246 void VCmdBuf::Exec () {
1247 //GCon->Logf(NAME_Debug, "=============================\nEXECBUF: \"%s\"\n----------------", *Buffer.quote());
1248 while (Buffer.length()) {
1249 if (checkWait()) {
1250 //GCon->Logf(NAME_Debug, "*** WAIT HIT! bufleft: \"%s\"", *Buffer.quote());
1251 break;
1254 int quotes = 0;
1255 bool comment = false;
1256 int len = 0;
1257 int stpos = -1;
1258 const int blen = Buffer.length();
1259 const char *bs = *Buffer;
1261 for (; len < blen; ++len) {
1262 const char ch = *bs++;
1263 // EOL?
1264 if (ch == '\n') {
1265 if (stpos >= 0) break;
1266 vassert(!quotes);
1267 comment = false;
1268 continue;
1270 // in the comment?
1271 if (comment) continue;
1272 // inside the quotes?
1273 if (quotes) {
1274 if (ch == quotes) quotes = 0;
1275 else if (ch == '\\') { ++bs; ++len; } // skip escaping (it is safe to not check `len` here)
1276 continue;
1278 // separator?
1279 if (ch == ';') {
1280 if (stpos >= 0) break;
1281 continue;
1283 // comment? (it is safe to check `*bs` here)
1284 if (ch == '/' && *bs == '/') {
1285 if (stpos >= 0) break; // comment will be skipped on the next iteration
1286 comment = true;
1287 continue;
1289 // if non-blank char, mark string start
1290 if ((vuint8)ch > ' ' && stpos < 0) stpos = len;
1291 if (ch == '"' || ch == '\'') quotes = ch; // quoting
1292 // tokenizer doesn't allow esaping outside of quotes
1293 //else if (ch == '\\') { ++bs; ++len; } // skip escaping (it is safe to not check `len` here)
1296 if (stpos < 0) {
1297 // no non-blanks
1298 if (len > 0) Buffer.chopLeft(len);
1299 //GCon->Logf(NAME_Debug, "*** CHOPPED: \"%s\"", *Buffer.quote());
1300 continue;
1302 vassert(len);
1304 VStr ParsedCmd(*Buffer+stpos, len-stpos);
1305 Buffer.chopLeft(len);
1307 //GCon->Logf(NAME_Debug, "*** CMD: \"%s\"", *ParsedCmd.quote());
1308 //GCon->Logf(NAME_Debug, "*** chopped: \"%s\"", *Buffer.quote());
1309 // safety net
1310 const int prevlen = Buffer.length();
1311 VCommand::ExecuteString(ParsedCmd, VCommand::SRC_Command, nullptr);
1313 if (host_request_exit) return;
1315 // check length
1316 const int currlen = Buffer.length();
1317 if (currlen > prevlen && currlen > 1024*1024*16) Sys_Error("console command buffer overflow (probably invalid alias or something)");
1322 //**************************************************************************
1324 // Some commands
1326 //**************************************************************************
1328 //==========================================================================
1330 // COMMAND CmdList
1332 //==========================================================================
1333 COMMAND(CmdList) {
1334 const char *prefix = (Args.length() > 1 ? *Args[1] : "");
1335 int pref_len = VStr::Length(prefix);
1336 int count = 0;
1337 for (VCommand *cmd = Cmds; cmd; cmd = cmd->Next) {
1338 if (pref_len && VStr::NICmp(cmd->Name, prefix, pref_len)) continue;
1339 GCon->Logf(" %s", cmd->Name);
1340 ++count;
1342 GCon->Logf("%d commands.", count);
1346 //==========================================================================
1348 // VCommand::rebuildAliasMap
1350 //==========================================================================
1351 void VCommand::rebuildAliasMap () {
1352 AliasMap.reset();
1353 for (auto &&it : AliasList.itemsIdx()) {
1354 VAlias &al = it.value();
1355 VStr aliasName = al.Name/*.toLowerCase()*/;
1356 if (aliasName.length() == 0) continue; // just in case
1357 AliasMap.put(aliasName, it.index());
1362 //==========================================================================
1364 // Alias_f
1366 //==========================================================================
1367 COMMAND(Alias) {
1368 if (Args.length() == 1) {
1369 GCon->Logf("\034K%s", "Current aliases:");
1370 for (auto &&al : AliasList) {
1371 GCon->Logf("\034D %s: %s%s", *al.Name, *al.CmdLine, (al.Save ? "" : " (temporary)"));
1373 return;
1376 //VStr aliasName = Args[1].toLowerCase();
1377 auto idxp = AliasMap.get(/*aliasName*/Args[1]);
1379 if (Args.length() == 2) {
1380 if (!idxp) {
1381 GCon->Logf("no named alias '%s' found", *Args[1]);
1382 } else {
1383 const VAlias &al = AliasList[*idxp];
1384 GCon->Logf("alias %s: %s%s", *Args[1], *al.CmdLine, (al.Save ? "" : " (temporary)"));
1386 return;
1389 VStr cmd;
1390 for (int f = 2; f < Args.length(); ++f) {
1391 if (Args[f].isEmpty()) continue;
1392 if (cmd.length()) cmd += ' ';
1393 cmd += Args[f];
1395 cmd = cmd.xstrip(); // why not?
1397 if (cmd.isEmpty()) {
1398 if (Args.length() != 3 || !Args[2].isEmpty()) {
1399 GCon->Logf("invalid alias defintion for '%s' (empty command)", *Args[1]);
1400 return;
1402 // remove alias
1403 if (!idxp) {
1404 GCon->Logf("no named alias '%s' found", *Args[1]);
1405 } else {
1406 VAlias &al = AliasList[*idxp];
1407 if (ParsingKeyConf && al.Save) {
1408 GCon->Logf("cannot remove permanent alias '%s' from keyconf", *Args[1]);
1409 } else {
1410 AliasList.removeAt(*idxp);
1411 rebuildAliasMap();
1412 if (GetCommandType(Args[1]) == CT_UNKNOWN) RemoveFromAutoComplete(*Args[1]);
1413 GCon->Logf("removed alias '%s'", *Args[1]);
1416 return;
1419 if (idxp) {
1420 // redefine alias
1421 VAlias &al = AliasList[*idxp];
1422 if (ParsingKeyConf && al.Save) {
1423 // add new temporary alias below
1424 } else {
1425 // redefine alias
1426 al.CmdLine = cmd;
1427 GCon->Logf("redefined alias '%s'", *Args[1]);
1428 return;
1432 // new alias
1433 VAlias &nal = AliasList.alloc();
1434 nal.Name = Args[1];
1435 nal.CmdLine = cmd;
1436 nal.Save = !ParsingKeyConf;
1438 if (ParsingKeyConf) {
1439 if (ParsingKeyConf) {
1440 GCon->Logf(NAME_Init, "defined temporary alias '%s': %s", *Args[1], *cmd);
1441 } else {
1442 GCon->Logf("defined alias '%s': %s", *Args[1], *cmd);
1446 rebuildAliasMap();
1447 AddToAutoComplete(*Args[1]);
1451 //==========================================================================
1453 // COMMAND echo
1455 //==========================================================================
1456 COMMAND(echo) {
1457 if (Args.length() < 2) return;
1459 VStr Text = Args[1];
1460 for (int i = 2; i < Args.length(); ++i) {
1461 Text += " ";
1462 Text += Args[i];
1464 Text = Text.EvalEscapeSequences();
1465 #ifdef CLIENT
1466 if (cl) {
1467 cl->Printf("%s", *Text);
1469 else
1470 #endif
1472 GCon->Log(Text);
1477 //==========================================================================
1479 // COMMAND exec
1481 //==========================================================================
1482 COMMAND(exec) {
1483 if (Args.length() < 2 || Args.length() > 3) {
1484 GCon->Log((execLogInit ? NAME_Init : NAME_Log), "exec <filename> [nofilerrors]: execute script file");
1485 return;
1488 //GCon->Logf(NAME_Debug, "***EXEC***: execLogInit=%d; fname=<%s>", (int)execLogInit, *Args[1]);
1490 // check for some special files
1491 enum {
1492 NormalRC,
1493 SpecialK8,
1494 SpecialUser,
1497 int ftype = NormalRC;
1498 VStr basename = Args[1].extractFileBaseName();
1499 if (basename.strEquCI("k8vavoom_startup.vs") || basename.strEquCI("k8vavoom_default.cfg")) {
1500 if (!Args[1].strEquCI(basename)) {
1501 GCon->Logf(NAME_Error, "cannot execute special init file '%s' with a path!", *Args[1]);
1502 return;
1504 ftype = SpecialK8;
1505 } else if (basename.strEquCI("config.cfg") || basename.strEquCI("autoexec.cfg")) {
1506 if (!Args[1].strEquCI(basename)) {
1507 GCon->Logf(NAME_Error, "cannot execute special user init file '%s' with a path!", *Args[1]);
1508 return;
1510 ftype = SpecialUser;
1513 VStream *Strm = nullptr;
1515 // try disk file (except for standard init files)
1516 if (ftype != SpecialK8) {
1517 VStr dskname = Host_GetConfigDir()+"/"+Args[1];
1518 Strm = FL_OpenSysFileRead(dskname);
1519 if (Strm) {
1520 GCon->Logf((execLogInit ? NAME_Init : NAME_Log), "executing %s file '%s'...",
1521 (ftype == SpecialK8 ? "WRONG init" : ftype == SpecialUser ? "init" : "disk"), *dskname);
1525 // try wad file (except for special user init files)
1526 if (!Strm && ftype != SpecialUser) {
1527 int lump = -1;
1528 // if we're still it "initial startup" mode, allow only base paks
1529 Strm = (execLogInit || ftype != NormalRC ? FL_OpenFileReadBaseOnly(Args[1], &lump) : FL_OpenFileRead(Args[1], &lump));
1530 if (Strm) {
1531 //GCon->Logf((execLogInit ? NAME_Init : NAME_Log), "executing '%s'...", *Args[1]);
1532 GCon->Logf((execLogInit ? NAME_Init : NAME_Log), "executing %s file '%s'...",
1533 (ftype == SpecialK8 ? "init" : ftype == SpecialUser ? "WRONG init" : "rc"), *W_FullLumpName(lump));
1534 //GCon->Logf("<%s>", *Strm->GetName());
1538 if (!Strm) {
1539 if (Args.length() == 2) GCon->Logf(NAME_Warning, "Can't find '%s'", *Args[1]);
1540 return;
1543 //if (ftype == SpecialUser) GCon->Logf("Executing '%s'", *Args[1]);
1545 int flsize = Strm->TotalSize();
1546 if (flsize == 0) { VStream::Destroy(Strm); return; }
1547 if (flsize < 0 || flsize > 1024*1024*8) {
1548 VStream::Destroy(Strm);
1549 GCon->Logf(NAME_Warning, "rc file '%s' is too big, ignored.", *Args[1]);
1550 return;
1553 char *buf = new char[flsize+2];
1554 try {
1555 Strm->Serialise(buf, flsize);
1556 } catch (...) {
1557 VStream::Destroy(Strm);
1558 delete[] buf;
1559 throw;
1561 if (Strm->IsError()) {
1562 delete[] buf;
1563 VStream::Destroy(Strm);
1564 GCon->Logf(NAME_Warning, "Error reading '%s'!", *Args[1]);
1565 return;
1567 VStream::Destroy(Strm);
1569 if (buf[flsize-1] != '\n') buf[flsize++] = '\n';
1570 buf[flsize] = 0;
1572 GCmdBuf.Insert(buf);
1573 delete[] buf;
1577 //==========================================================================
1579 // COMMAND wait
1581 //==========================================================================
1582 COMMAND(wait) {
1583 int msecs = 0;
1584 if (Args.length() > 1) {
1585 if (!VStr::convertInt(*Args[1], &msecs)) msecs = 0;
1588 // negative means "milliseconds"
1589 if (msecs < 0) {
1590 // no more than 3 seconds
1591 if (msecs < -3000) msecs = -3000;
1592 msecs = -msecs;
1593 GCmdBuf.WaitEndTime = Sys_Time()+((double)msecs/1000.0);
1594 return;
1597 // positive means "tics" (roughly)
1598 if (msecs > 0) {
1599 // no more than 3 seconds
1600 if (msecs > 3*35) msecs = 10*35;
1601 GCmdBuf.WaitEndTime = Sys_Time()+((double)msecs/35.0+0.000001); // ticks (roughly)
1602 return;
1605 // error or zero
1606 GCmdBuf.WaitEndTime = Sys_Time()+(1.0/35.0+0.000001); // roughly one tick
1607 return;
1611 //==========================================================================
1613 // __k8_run_first_map
1615 // used for "-k8runmap" if mapinfo found
1617 //==========================================================================
1618 COMMAND(__k8_run_first_map) {
1619 if (P_GetNumEpisodes() == 0) {
1620 GCon->Logf("ERROR: No eposode info found!");
1621 return;
1624 VName startMap = NAME_None;
1625 //int bestmdlump = -1;
1627 for (int ep = 0; ep < P_GetNumEpisodes(); ++ep) {
1628 VEpisodeDef *edef = P_GetEpisodeDef(ep);
1629 if (!edef) continue; // just in case
1631 //GCon->Logf(NAME_Debug, "ep=%d: Name=<%s>; TeaserName=<%s>; Text=%s; MapinfoSourceLump=%d (%d) <%s>", ep, *edef->Name, *edef->TeaserName, *edef->Text.quote(true), edef->MapinfoSourceLump, W_IsUserWadLump(edef->MapinfoSourceLump), *W_FullLumpName(edef->MapinfoSourceLump));
1633 VName map = edef->Name;
1634 if (map == NAME_None || !IsMapPresent(map)) {
1635 //GCon->Logf(NAME_Debug, " ep=%d; map '%s' is not here!", ep, *edef->Name);
1636 map = edef->TeaserName;
1637 if (map == NAME_None || !IsMapPresent(map)) continue;
1640 else {
1641 GCon->Logf(NAME_Debug, " ep=%d; map '%s' is here!", ep, *edef->Name);
1645 const VMapInfo &mi = P_GetMapInfo(map);
1646 //GCon->Logf(NAME_Debug, " ep=%d; map '%s': LumpName=<%s> (%s); LevelNum=%d", ep, *map, *mi.LumpName, *mi.Name, mi.LevelNum); mi.dump("!!!");
1648 //k8: i don't care about levelnum here
1650 if (mi.LevelNum == 0) {
1651 //GCon->Logf(NAME_Debug, " ep=%d; map '%s' has zero levelnum!", ep, *map);
1652 continue;
1656 if (!W_IsUserWadLump(mi.MapinfoSourceLump)) continue; // ignore non-user maps
1658 // don't trust wad file order, use episode order instead
1659 //if (mi.MapinfoSourceLump <= bestmdlump) continue;
1660 //GCon->Logf(NAME_Debug, "MapinfoSourceLump=%d; name=<%s>; index=%d", mi.MapinfoSourceLump, *map, mi.LevelNum);
1662 //bestmdlump = mi.MapinfoSourceLump;
1663 startMap = map;
1664 break;
1667 if (startMap == NAME_None && fsys_PWadMaps.length()) {
1668 GCon->Log(NAME_Init, "cannot find starting map from mapinfo, taking from pwad map list");
1669 startMap = VName(*fsys_PWadMaps[0].mapname);
1672 if (startMap == NAME_None) {
1673 GCon->Log(NAME_Warning, "Starting map not found!");
1674 return;
1677 GCon->Logf(NAME_Init, "autostart map '%s'", *startMap);
1679 Host_CLIMapStartFound();
1680 GCmdBuf.Insert(va("map \"%s\"\n", *VStr(startMap).quote()));