1 //**************************************************************************
3 //** ## ## ## ## ## #### #### ### ###
4 //** ## ## ## ## ## ## ## ## ## ## #### ####
5 //** ## ## ## ## ## ## ## ## ## ## ## ## ## ##
6 //** ## ## ######## ## ## ## ## ## ## ## ### ##
7 //** ### ## ## ### ## ## ## ## ## ##
8 //** # ## ## # #### #### ## ##
10 //** Copyright (C) 1999-2006 Jānis Legzdiņš
11 //** Copyright (C) 2018-2023 Ketmar Dark
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.
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.
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/>.
25 //**************************************************************************
27 #include "psim/p_player.h"
28 #include "server/server.h"
31 #include "filesys/files.h"
32 #include "sound/sound.h"
35 # include "client/client.h"
38 extern VCvarB sv_cheats
;
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
[] = {
85 static bool wasRunCliCommands
= false;
88 //==========================================================================
92 //==========================================================================
93 static int sortCmpVStrCI (const void *a
, const void *b
, void * /*udata*/) {
101 //==========================================================================
105 //==========================================================================
106 static bool isBoolTrueStr (VStr s
) {
107 if (s
.isEmpty()) return false;
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) {
116 if (s.convertFloat(&f)) return (f != 0.0f);
119 if (s.convertInt(&n)) return (n != 0);
123 return VCvar::ParseBool(*s
);
127 //**************************************************************************
131 //**************************************************************************
133 //==========================================================================
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!");
144 // client will forward them anyway
145 if (GGameInfo
->NetMode
== NM_Client
) return true;
146 if (GGameInfo
->NetMode
== NM_ListenServer
|| GGameInfo
->NetMode
== NM_DedicatedServer
) {
148 Player
->Printf("You cannot cheat in a network game!");
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!");
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!
167 if (!allowDead
&& Player
->Health
<= 0) {
168 // dead players can't cheat
169 Player
->Printf("You must be alive to cheat");
176 //==========================================================================
178 // VCommand::VCommand
180 //==========================================================================
181 VCommand::VCommand (const char *name
) {
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 //==========================================================================
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");
225 //==========================================================================
227 // VCommand::InsertCLICommands
229 //==========================================================================
230 void VCommand::InsertCLICommands () {
233 if (!cliPreCmds
.isEmpty()) {
235 if (!cstr
.endsWith("\n")) cstr
+= '\n';
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 +
242 for (int i
= 1; i
< GArgs
.Count(); ++i
) {
245 if (GArgs
[i
] && (GArgs
[i
][0] == '-' || GArgs
[i
][0] == '+')) {
247 if (VStr::convertFloat(GArgs
[i
], &v
)) {
250 cstr
+= VStr(GArgs
[i
]).quote();
255 if (!GArgs
[i
] || GArgs
[i
][0] == '-' || GArgs
[i
][0] == '+') {
260 //GCmdBuf << " \"" << VStr(GArgs[i]).quote() << "\"";
263 cstr
+= VStr(GArgs
[i
]).quote();
268 if (GArgs
[i
][0] == '+') {
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;
299 //==========================================================================
301 // VCommand::WriteAlias
303 //==========================================================================
304 void VCommand::WriteAlias (VStream
*st
) {
306 TArray
<VAlias
*> alist
;
307 for (auto &&al
: AliasList
) if (al
.Save
) alist
.append(&al
);
308 if (alist
.length() == 0) return;
310 smsort_r(alist
.ptr(), alist
.length(), sizeof(VAlias
*), &vapcmp
, nullptr);
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 () {
326 AutoCompleteTable
.Clear();
327 AutoCompleteTableBSet
.clear();
334 //==========================================================================
336 // VCommand::LoadKeyconfLump
338 //==========================================================================
339 void VCommand::LoadKeyconfLump (int Lump
) {
340 if (Lump
< 0) return;
342 VStream
*Strm
= W_CreateLumpReaderNum(Lump
);
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
));
357 buf
.split('\n', lines
);
358 for (auto &&s
: lines
) {
360 if (s
.length() == 0 || s
[0] == '#' || s
[0] == '/') continue;
363 if (args
.length() == 0) continue;
365 if (args[0].strEquCI("defaultbind")) {
366 GCon->Logf(NAME_Warning, "ignored keyconf command: %s", *s);
374 // enable special mode for console commands
375 ParsingKeyConf
= true;
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;
406 VStr vslow
= vs
.toLowerCase();
408 if (AutoCompleteTableBSet
.has(vslow
)) return;
410 AutoCompleteTableBSet
.put(vslow
, true);
411 AutoCompleteTable
.Append(vs
);
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;
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
);
445 //==========================================================================
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
));
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;
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());
491 while (mlpos
< bestmatch
.length()) {
492 if (VStr::upcase1251(bestmatch
[mlpos
]) != VStr::upcase1251(mt
[mlpos
])) {
493 bestmatch
= bestmatch
.left(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; }
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
);
530 for (int f
= 0; f
< list
.length(); ++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 //==========================================================================
550 //==========================================================================
551 static VBasePlayer
*findPlayer () {
552 if (sv
.intermission
) return nullptr;
553 if (GGameInfo
->NetMode
< NM_Standalone
) return nullptr; // not playing
555 if (GGameInfo
->NetMode
== NM_Client
) return (cl
&& cl
->Net
? cl
: nullptr);
557 // find any active player
558 for (int f
= 0; f
< MAXPLAYERS
; ++f
) {
559 VBasePlayer
*plr
= GGameInfo
->Players
[f
];
561 if ((plr
->PlayerFlags
&VBasePlayer::PF_IsBot
) ||
562 !(plr
->PlayerFlags
&VBasePlayer::PF_Spawned
))
566 if (plr
->PlayerState
!= PST_LIVE
|| plr
->Health
<= 0) continue;
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);
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();
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
) {
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
617 rebuildCommandCache();
619 auto cptr
= locaseCache
.get(args
[0]);
621 VCommand
*cmd
= *cptr
;
622 VStr ac
= cmd
->AutoCompleteArg(args
, aidx
);
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);
629 for (int f
= 0; f
< aidx
; ++f
) {
630 res
+= args
[f
].quote(true); // add quote chars if necessary
636 if (!addSpace && ac[ac.length()-1] == '"') ac.chopRight(1);
639 if (addSpace) res += ' ';
644 // cannot complete, nothing's changed
650 VBasePlayer
*plr
= findPlayer();
653 if (plr
->ExecConCommandAC(args
, endsWithBlank
, aclist
)) {
654 if (aclist
.length() == 0) return prefix
; // nothing's found
657 for (int f
= 0; f
< aidx
; ++f
) {
658 res
+= args
[f
].quote(true); // add quote chars if necessary
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);
671 if (!addSpace && ac[ac.length()-1] == '"') ac.chopRight(1);
675 if (addSpace) res += ' ';
686 // show cvar help, why not?
687 if (onShowCompletionMatch
) {
688 VCvar
*var
= VCvar::FindVariable(*args
[0]);
690 VStr help
= var
->GetHelp();
691 if (help
.length() && !VStr(help
).startsWithNoCase("no help yet")) {
692 onShowCompletionMatch(false, args
[0]+": "+help
);
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
;
724 const char ch
= *s
++;
725 if (ch
== '$' && (*s
!= '(' || *s
== '{')) {
726 const char *sstart
= s
-1;
727 const char ech
= (*s
== '{' ? '}' : ')');
731 while (*se
&& *se
!= ech
) ++se
;
733 VStr
cvn(s
, (int)(ptrdiff_t)(se
-s
));
735 if (cvn
.length() && cvn
[0] >= '0' && cvn
[0] <= '9') {
738 if (cvn
.length() > 1 && cvn
[cvn
.length()-1] == 'q') {
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
];
752 res
+= VStr(sstart
, (int)(ptrdiff_t)(se
-sstart
));
756 if (ch
== '\\' && *s
) res
+= *s
++;
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
;
774 const char ch
= *s
++;
775 if (ch
== '$' && (*s
!= '(' || *s
== '{')) {
776 const char ech
= (*s
== '{' ? '}' : ')');
780 while (*se
&& *se
!= ech
) ++se
;
782 VStr
cvn(s
, (int)(ptrdiff_t)(se
-s
));
784 VCvar
*cv
= VCvar::FindVariable(*cvn
);
785 if (cv
) res
+= cv
->asStr();
790 if (ch
== '\\' && *s
) res
+= *s
++;
797 //==========================================================================
799 // VCommand::TokeniseString
801 //==========================================================================
802 void VCommand::TokeniseString (VStr str
) {
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;
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
;
854 //==========================================================================
856 // VCommand::ProcessSetCommand
858 // "set [(type)] var value" (i.e. "set (int) myvar 0")
864 // default is "(string)"
866 //==========================================================================
867 void VCommand::ProcessSetCommand () {
875 if (Args
.length() < 3 || Args
.length() > 4) {
876 if (host_initialised
) GCon
->Log(NAME_Error
, "'set' command usage: \"set [(type)] name value\"");
883 if (Args
.length() == 4) {
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
;
891 if (host_initialised
) GCon
->Logf(NAME_Error
, "invalid variable type `%s` in 'set'", *Args
[1]);
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");
910 if (!vvalue
.convertInt(&iv
)) {
911 if (host_initialised
) GCon
->Logf(NAME_Error
, "'set' expects int, but got `%s`", *vvalue
);
916 if (!vvalue
.convertFloat(&fv
)) {
917 if (host_initialised
) GCon
->Logf(NAME_Error
, "'set' expects float, but got `%s`", *vvalue
);
922 bv
= isBoolTrueStr(vvalue
);
926 default: Sys_Error("VCommand::ProcessSetCommand: wtf vconvert?!");
929 VCvar
*cv
= VCvar::FindVariable(*vname
);
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?!");
939 if (cv
->IsReadOnly()) {
940 if (host_initialised
) GCon
->Logf(NAME_Error
, "'set' tried to modify read-only cvar '%s'", *vname
);
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
);
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
978 GSoundManager
->InitThreads();
981 if (!R_IsDrawerInited()) {
982 wasRunCliCommands
= false;
983 GCmdBuf
.Insert("wait\n__run_cli_commands__\n");
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
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
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
1027 if (ccmd
.isEmpty()) return; // just in case
1030 if (ParsingKeyConf
) {
1031 // verify that it's a valid keyconf command
1033 for (unsigned i
= 0; i
< ARRAY_COUNT(KeyConfCommands
); ++i
) {
1034 if (ccmd
.strEquCI(KeyConfCommands
[i
])) {
1040 GCon
->Logf(NAME_Warning
, "Invalid KeyConf command: %s", *Acmd
);
1045 if (ccmd
.strEquCI("set")) {
1046 ProcessSetCommand();
1050 // check for command
1051 rebuildCommandCache();
1053 auto cptr
= locaseCache
.get(ccmd
);
1055 if (cptr
&& !Player
) Player
= findPlayer(); // for local commands
1060 // check for player command
1061 if (Source
== SRC_Command
) {
1062 VBasePlayer
*plr
= findPlayer();
1063 if (plr
&& plr
->IsConCommand(ccmd
)) {
1067 } else if (Player
&& Player
->IsConCommand(ccmd
)) {
1068 if (CheatAllowed(Player
)) Player
->ExecConCommand();
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();
1081 if (!plr
|| !cl
|| !cl
->Net
)
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
);
1094 if (cheatingChanged
) VCvar::Unlatch();
1099 // check for command defined with ALIAS
1100 if (!ccmd
.isEmpty()) {
1101 auto idp
= AliasMap
.get(ccmd
);
1103 VAlias
&al
= AliasList
[*idp
];
1104 GCmdBuf
.Insert("\n");
1105 GCmdBuf
.Insert(SubstituteArgs(al
.CmdLine
));
1112 if (host_initialised
)
1114 GCon
->Logf(NAME_Error
, "Unknown command '%s'", *ccmd
);
1118 //==========================================================================
1120 // VCommand::ForwardToServer
1122 //==========================================================================
1123 void VCommand::ForwardToServer () {
1126 GCon
->Log("You must be in a game to execute this command");
1129 if (!CL_SendCommandToServer(Original
)) {
1130 VCommand::ExecuteString(Original
, VCommand::SRC_Client
, cl
);
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);
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
;
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();
1177 //**************************************************************************
1181 //**************************************************************************
1183 //==========================================================================
1187 //==========================================================================
1188 void VCmdBuf::Insert (const char *text
) {
1189 if (text
&& text
[0]) Buffer
= VStr(text
)+Buffer
;
1193 //==========================================================================
1197 //==========================================================================
1198 void VCmdBuf::Insert (VStr text
) {
1199 if (text
.length()) Buffer
= text
+Buffer
;
1203 //==========================================================================
1207 //==========================================================================
1208 void VCmdBuf::Print (const char *data
) {
1209 if (data
&& data
[0]) Buffer
+= data
;
1213 //==========================================================================
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
) {
1241 //==========================================================================
1245 //==========================================================================
1246 void VCmdBuf::Exec () {
1247 //GCon->Logf(NAME_Debug, "=============================\nEXECBUF: \"%s\"\n----------------", *Buffer.quote());
1248 while (Buffer
.length()) {
1250 //GCon->Logf(NAME_Debug, "*** WAIT HIT! bufleft: \"%s\"", *Buffer.quote());
1255 bool comment
= false;
1258 const int blen
= Buffer
.length();
1259 const char *bs
= *Buffer
;
1261 for (; len
< blen
; ++len
) {
1262 const char ch
= *bs
++;
1265 if (stpos
>= 0) break;
1271 if (comment
) continue;
1272 // inside the quotes?
1274 if (ch
== quotes
) quotes
= 0;
1275 else if (ch
== '\\') { ++bs
; ++len
; } // skip escaping (it is safe to not check `len` here)
1280 if (stpos
>= 0) break;
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
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)
1298 if (len
> 0) Buffer
.chopLeft(len
);
1299 //GCon->Logf(NAME_Debug, "*** CHOPPED: \"%s\"", *Buffer.quote());
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());
1310 const int prevlen
= Buffer
.length();
1311 VCommand::ExecuteString(ParsedCmd
, VCommand::SRC_Command
, nullptr);
1313 if (host_request_exit
) return;
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 //**************************************************************************
1326 //**************************************************************************
1328 //==========================================================================
1332 //==========================================================================
1334 const char *prefix
= (Args
.length() > 1 ? *Args
[1] : "");
1335 int pref_len
= VStr::Length(prefix
);
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
);
1342 GCon
->Logf("%d commands.", count
);
1346 //==========================================================================
1348 // VCommand::rebuildAliasMap
1350 //==========================================================================
1351 void VCommand::rebuildAliasMap () {
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 //==========================================================================
1366 //==========================================================================
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)"));
1376 //VStr aliasName = Args[1].toLowerCase();
1377 auto idxp
= AliasMap
.get(/*aliasName*/Args
[1]);
1379 if (Args
.length() == 2) {
1381 GCon
->Logf("no named alias '%s' found", *Args
[1]);
1383 const VAlias
&al
= AliasList
[*idxp
];
1384 GCon
->Logf("alias %s: %s%s", *Args
[1], *al
.CmdLine
, (al
.Save
? "" : " (temporary)"));
1390 for (int f
= 2; f
< Args
.length(); ++f
) {
1391 if (Args
[f
].isEmpty()) continue;
1392 if (cmd
.length()) cmd
+= ' ';
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]);
1404 GCon
->Logf("no named alias '%s' found", *Args
[1]);
1406 VAlias
&al
= AliasList
[*idxp
];
1407 if (ParsingKeyConf
&& al
.Save
) {
1408 GCon
->Logf("cannot remove permanent alias '%s' from keyconf", *Args
[1]);
1410 AliasList
.removeAt(*idxp
);
1412 if (GetCommandType(Args
[1]) == CT_UNKNOWN
) RemoveFromAutoComplete(*Args
[1]);
1413 GCon
->Logf("removed alias '%s'", *Args
[1]);
1421 VAlias
&al
= AliasList
[*idxp
];
1422 if (ParsingKeyConf
&& al
.Save
) {
1423 // add new temporary alias below
1427 GCon
->Logf("redefined alias '%s'", *Args
[1]);
1433 VAlias
&nal
= AliasList
.alloc();
1436 nal
.Save
= !ParsingKeyConf
;
1438 if (ParsingKeyConf
) {
1439 if (ParsingKeyConf
) {
1440 GCon
->Logf(NAME_Init
, "defined temporary alias '%s': %s", *Args
[1], *cmd
);
1442 GCon
->Logf("defined alias '%s': %s", *Args
[1], *cmd
);
1447 AddToAutoComplete(*Args
[1]);
1451 //==========================================================================
1455 //==========================================================================
1457 if (Args
.length() < 2) return;
1459 VStr Text
= Args
[1];
1460 for (int i
= 2; i
< Args
.length(); ++i
) {
1464 Text
= Text
.EvalEscapeSequences();
1467 cl
->Printf("%s", *Text
);
1477 //==========================================================================
1481 //==========================================================================
1483 if (Args
.length() < 2 || Args
.length() > 3) {
1484 GCon
->Log((execLogInit
? NAME_Init
: NAME_Log
), "exec <filename> [nofilerrors]: execute script file");
1488 //GCon->Logf(NAME_Debug, "***EXEC***: execLogInit=%d; fname=<%s>", (int)execLogInit, *Args[1]);
1490 // check for some special files
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]);
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]);
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
);
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
) {
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
));
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());
1539 if (Args
.length() == 2) GCon
->Logf(NAME_Warning
, "Can't find '%s'", *Args
[1]);
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]);
1553 char *buf
= new char[flsize
+2];
1555 Strm
->Serialise(buf
, flsize
);
1557 VStream::Destroy(Strm
);
1561 if (Strm
->IsError()) {
1563 VStream::Destroy(Strm
);
1564 GCon
->Logf(NAME_Warning
, "Error reading '%s'!", *Args
[1]);
1567 VStream::Destroy(Strm
);
1569 if (buf
[flsize
-1] != '\n') buf
[flsize
++] = '\n';
1572 GCmdBuf
.Insert(buf
);
1577 //==========================================================================
1581 //==========================================================================
1584 if (Args
.length() > 1) {
1585 if (!VStr::convertInt(*Args
[1], &msecs
)) msecs
= 0;
1588 // negative means "milliseconds"
1590 // no more than 3 seconds
1591 if (msecs
< -3000) msecs
= -3000;
1593 GCmdBuf
.WaitEndTime
= Sys_Time()+((double)msecs
/1000.0);
1597 // positive means "tics" (roughly)
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)
1606 GCmdBuf
.WaitEndTime
= Sys_Time()+(1.0/35.0+0.000001); // roughly one tick
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!");
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;
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);
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;
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!");
1677 GCon
->Logf(NAME_Init
, "autostart map '%s'", *startMap
);
1679 Host_CLIMapStartFound();
1680 GCmdBuf
.Insert(va("map \"%s\"\n", *VStr(startMap
).quote()));