mirror of
https://github.com/odamex/odamex
synced 2026-08-19 02:23:09 -04:00
756 lines
16 KiB
C++
756 lines
16 KiB
C++
// Emacs style mode select -*- C++ -*-
|
|
//-----------------------------------------------------------------------------
|
|
//
|
|
// $Id$
|
|
//
|
|
// Copyright (C) 1997-2000 by id Software Inc.
|
|
// Copyright (C) 1998-2006 by Randy Heit (ZDoom).
|
|
// Copyright (C) 2006-2026 by The Odamex Team.
|
|
//
|
|
// This program is free software; you can redistribute it and/or
|
|
// modify it under the terms of the GNU General Public License
|
|
// as published by the Free Software Foundation; either version 2
|
|
// of the License, or (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// DESCRIPTION:
|
|
// Command library (mostly borrowed from the Q2 source)
|
|
//
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
|
#include "odamex.h"
|
|
|
|
#include <math.h>
|
|
#include <stdarg.h>
|
|
#include <stdlib.h>
|
|
|
|
#include <ctime>
|
|
#include <cctype>
|
|
#include <functional>
|
|
#include <sstream>
|
|
|
|
#include "win32inc.h"
|
|
|
|
#include "i_system.h"
|
|
#include "cmdlib.h"
|
|
|
|
#include "fmt/ranges.h"
|
|
|
|
#ifdef __SWITCH__
|
|
#include "nx_system.h"
|
|
#endif
|
|
|
|
// Safe string copy function that works like OpenBSD's strlcpy().
|
|
// Returns true if the string was not truncated.
|
|
// from Chocolate Doom m_misc.cpp
|
|
|
|
bool M_StringCopy(char *dest, const char *src, size_t dest_size)
|
|
{
|
|
size_t len;
|
|
|
|
if (dest_size >= 1)
|
|
{
|
|
dest[dest_size - 1] = '\0';
|
|
strncpy(dest, src, dest_size - 1);
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
|
|
len = strlen(dest);
|
|
return src[len] == '\0';
|
|
}
|
|
|
|
char *copystring (const char *s)
|
|
{
|
|
char *b;
|
|
if (s)
|
|
{
|
|
size_t len = strlen(s) + 1;
|
|
b = new char[len];
|
|
M_StringCopy(b, s, len);
|
|
}
|
|
else
|
|
{
|
|
b = new char[1];
|
|
b[0] = '\0';
|
|
}
|
|
return b;
|
|
}
|
|
|
|
//
|
|
// ParseNum / ParseHex
|
|
//
|
|
int ParseHex(const char* hex)
|
|
{
|
|
int num = 0;
|
|
const char* str = hex;
|
|
|
|
while (*str)
|
|
{
|
|
num <<= 4;
|
|
if (*str >= '0' && *str <= '9')
|
|
num += *str-'0';
|
|
else if (*str >= 'a' && *str <= 'f')
|
|
num += 10 + *str-'a';
|
|
else if (*str >= 'A' && *str <= 'F')
|
|
num += 10 + *str-'A';
|
|
else {
|
|
DPrintFmt("Bad hex number: {}\n",hex);
|
|
return 0;
|
|
}
|
|
str++;
|
|
}
|
|
|
|
return num;
|
|
}
|
|
|
|
//
|
|
// ParseNum
|
|
//
|
|
int ParseNum(const char* str)
|
|
{
|
|
if (str[0] == '$')
|
|
return ParseHex(str+1);
|
|
if (str[0] == '0' && str[1] == 'x')
|
|
return ParseHex(str+2);
|
|
return atol(str);
|
|
}
|
|
|
|
// [RH] Returns true if the specified string is a valid decimal number
|
|
|
|
bool IsNum(const char* str)
|
|
{
|
|
bool result = true;
|
|
|
|
while (*str)
|
|
{
|
|
if (((*str < '0') || (*str > '9')) && (*str != '-'))
|
|
{
|
|
result = false;
|
|
break;
|
|
}
|
|
str++;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
bool IsNum(std::string_view str)
|
|
{
|
|
return std::all_of(str.begin(), str.end(), [](char c)
|
|
{
|
|
if (((c < '0') || (c > '9')) && (c != '-'))
|
|
{
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
}
|
|
|
|
//
|
|
// IsRealNum
|
|
//
|
|
// [SL] Returns true if the specified string is a valid real number
|
|
//
|
|
bool IsRealNum(const char* str)
|
|
{
|
|
bool seen_decimal = false;
|
|
|
|
if (str == NULL || *str == 0)
|
|
return false;
|
|
|
|
if (str[0] == '+' || str[0] == '-')
|
|
str++;
|
|
|
|
while (*str)
|
|
{
|
|
if (*str == '.')
|
|
{
|
|
if (seen_decimal)
|
|
return false; // second decimal point
|
|
else
|
|
seen_decimal = true;
|
|
}
|
|
else if (*str < '0' || *str > '9')
|
|
return false;
|
|
|
|
str++;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//
|
|
// IsRealNum
|
|
//
|
|
// [SL] Returns true if the specified string is a valid real number
|
|
//
|
|
bool IsRealNum(std::string_view str)
|
|
{
|
|
bool seen_decimal = false;
|
|
|
|
if (str.empty())
|
|
return false;
|
|
|
|
if (str.starts_with('+') || str.starts_with('-'))
|
|
str.remove_prefix(1);
|
|
|
|
for (char c : str)
|
|
{
|
|
if (c == '.')
|
|
{
|
|
if (seen_decimal)
|
|
return false; // second decimal point
|
|
else
|
|
seen_decimal = true;
|
|
}
|
|
else if (c < '0' || c > '9')
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// [Russell] Returns 0 if strings are the same, optional parameter for case
|
|
// sensitivity
|
|
bool iequals(std::string_view s1, std::string_view s2)
|
|
{
|
|
if (s1.size() != s2.size())
|
|
return false;
|
|
return strnicmp(s1.data(), s2.data(), s1.size()) == 0;
|
|
}
|
|
|
|
size_t StdStringFind(const std::string& haystack, const std::string& needle,
|
|
size_t pos, size_t n, bool CIS, bool reverse)
|
|
{
|
|
if (CIS)
|
|
{
|
|
if(reverse)
|
|
{
|
|
return StdStringToUpper(haystack).rfind(StdStringToUpper(needle).c_str(), pos, n);
|
|
}
|
|
|
|
return StdStringToUpper(haystack).find(StdStringToUpper(needle).c_str(), pos, n);
|
|
}
|
|
|
|
if(reverse)
|
|
{
|
|
return haystack.rfind(needle.c_str(), pos, n);
|
|
}
|
|
|
|
return haystack.find(needle.c_str(), pos, n);
|
|
}
|
|
|
|
size_t StdStringFind(const std::string& haystack, const std::string& needle,
|
|
size_t pos, size_t n, bool CIS)
|
|
{
|
|
return StdStringFind(haystack, needle, pos, n, CIS, false);
|
|
}
|
|
|
|
size_t StdStringRFind(const std::string& haystack, const std::string& needle,
|
|
size_t pos, size_t n, bool CIS)
|
|
{
|
|
return StdStringFind(haystack, needle, pos, n, CIS, true);
|
|
}
|
|
|
|
static std::string& StdStringToLowerBase(std::string& lower, size_t n)
|
|
{
|
|
std::string::iterator itend = n >= lower.length() ? lower.end() : lower.begin() + n;
|
|
std::transform(lower.begin(), itend, lower.begin(), ::tolower);
|
|
return lower;
|
|
}
|
|
|
|
std::string StdStringToLower(const std::string& str, size_t n)
|
|
{
|
|
std::string lower(str, 0, n);
|
|
return StdStringToLowerBase(lower, n);
|
|
}
|
|
|
|
std::string StdStringToLower(const char* str, size_t n)
|
|
{
|
|
std::string lower(str, 0, n);
|
|
return StdStringToLowerBase(lower, n);
|
|
}
|
|
|
|
static std::string& StdStringToUpperBase(std::string& upper, size_t n)
|
|
{
|
|
std::string::iterator itend = n >= upper.length() ? upper.end() : upper.begin() + n;
|
|
std::transform(upper.begin(), itend, upper.begin(), ::toupper);
|
|
return upper;
|
|
}
|
|
|
|
std::string StdStringToUpper(const std::string& str, size_t n)
|
|
{
|
|
std::string upper(str, 0, n);
|
|
return StdStringToUpperBase(upper, n);
|
|
}
|
|
|
|
std::string StdStringToUpper(const char* str, size_t n)
|
|
{
|
|
std::string upper(str, 0, n);
|
|
return StdStringToUpperBase(upper, n);
|
|
}
|
|
|
|
// [AM] Convert an argc/argv into a vector of strings.
|
|
std::vector<std::string> VectorArgs(size_t argc, char **argv) {
|
|
std::vector<std::string> arguments(argc - 1);
|
|
for (unsigned i = 1;i < argc;i++) {
|
|
arguments[i - 1] = argv[i];
|
|
}
|
|
return arguments;
|
|
}
|
|
|
|
// [AM] Return a joined string based on a vector of strings
|
|
std::string JoinStrings(const std::vector<std::string> &pieces, const std::string &glue) {
|
|
return fmt::format("{}", fmt::join(pieces, glue));
|
|
}
|
|
|
|
// Tokenize a string
|
|
StringTokens TokenizeString(const std::string& str, const std::string& delim) {
|
|
StringTokens tokens;
|
|
|
|
if (str.empty())
|
|
return tokens;
|
|
|
|
size_t delimPos = 0;
|
|
size_t prevDelim = 0;
|
|
|
|
while (delimPos != std::string::npos) {
|
|
delimPos = str.find(delim, prevDelim);
|
|
tokens.push_back(str.substr(prevDelim, delimPos - prevDelim));
|
|
prevDelim = delimPos + delim.length();
|
|
}
|
|
|
|
return tokens;
|
|
}
|
|
|
|
/**
|
|
* @brief Format passed number of bytes with a byte multiple suffix.
|
|
*
|
|
* @param out Output string buffer.
|
|
* @param bytes Number of bytes to format.
|
|
*/
|
|
void StrFormatBytes(std::string& out, size_t bytes)
|
|
{
|
|
static const char* BYTE_MAGS[] = {
|
|
"B",
|
|
"kB",
|
|
"MB",
|
|
"GB",
|
|
};
|
|
|
|
size_t magnitude = 0;
|
|
double checkbytes = bytes;
|
|
while (checkbytes >= 1000.0 && magnitude < ARRAY_LENGTH(BYTE_MAGS))
|
|
{
|
|
magnitude += 1;
|
|
checkbytes /= 1000.0;
|
|
}
|
|
|
|
if (magnitude)
|
|
out = fmt::sprintf("%.2f %s", checkbytes, BYTE_MAGS[magnitude]);
|
|
else
|
|
out = fmt::sprintf("%.0f %s", checkbytes, BYTE_MAGS[magnitude]);
|
|
}
|
|
|
|
// TODO: update these to use c++20 std::chrono types and drop strptime
|
|
// [AM] Format a tm struct as an ISO8601-compliant extended format string.
|
|
// Assume that the input time is in UTC.
|
|
bool StrFormatISOTime(std::string& s, const tm* utc_tm) {
|
|
char buffer[21];
|
|
if (!strftime(buffer, 21, "%Y-%m-%dT%H:%M:%SZ", utc_tm)) {
|
|
return false;
|
|
}
|
|
s = buffer;
|
|
return true;
|
|
}
|
|
|
|
// [AM] Parse an ISO8601-formatted string time into a tm* struct.
|
|
bool StrParseISOTime(const std::string& s, tm* utc_tm) {
|
|
if (!strptime(s.c_str(), "%Y-%m-%dT%H:%M:%SZ", utc_tm)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// [AM] Turn a string representation of a length of time into a time_t
|
|
// relative to the current time.
|
|
bool StrToTime(std::string str, time_t &tim) {
|
|
tim = time(NULL);
|
|
str = TrimString(str);
|
|
str = StdStringToLower(str);
|
|
|
|
if (str.empty()) {
|
|
return false;
|
|
}
|
|
|
|
// We use 0 as a synonym for forever.
|
|
if (str.compare(std::string("eternity").substr(0, str.size())) == 0 ||
|
|
str.compare(std::string("forever").substr(0, str.size())) == 0 ||
|
|
str.compare(std::string("permanent").substr(0, str.size())) == 0) {
|
|
tim = 0;
|
|
return true;
|
|
}
|
|
|
|
// Gather tokens from string representation.
|
|
typedef std::pair<unsigned short, std::string> token_t;
|
|
typedef std::vector<token_t> tokens_t;
|
|
tokens_t tokens;
|
|
|
|
size_t i, j;
|
|
size_t size = str.size();
|
|
i = j = 0;
|
|
|
|
while (i < size) {
|
|
unsigned short num = 0;
|
|
std::string timeword;
|
|
|
|
// Grab a number.
|
|
j = i;
|
|
while (str[j] >= '0' && str[j] <= '9' && j < size) {
|
|
j++;
|
|
}
|
|
|
|
if (i == j) {
|
|
// There is no number.
|
|
return false;
|
|
}
|
|
|
|
if (!(j < size)) {
|
|
// We were expecting a number but ran off the end of the string.
|
|
return false;
|
|
}
|
|
|
|
std::istringstream num_buffer(str.substr(i, j - i));
|
|
num_buffer >> num;
|
|
|
|
i = j;
|
|
|
|
// Skip whitespace
|
|
while ((str[i] == ' ') && i < size) {
|
|
i++; j++;
|
|
}
|
|
|
|
// Grab a time word
|
|
while (str[j] >= 'a' && str[j] <= 'z' && j < size) {
|
|
j++;
|
|
}
|
|
|
|
if (i == j) {
|
|
// There is no time word.
|
|
return false;
|
|
}
|
|
|
|
timeword = str.substr(i, j - i);
|
|
i = j;
|
|
|
|
// Push to tokens vector
|
|
tokens.emplace_back(num, timeword);
|
|
|
|
// Skip whitespace and commas.
|
|
while ((str[i] == ' ' || str[i] == ',') && i < size) {
|
|
i++;
|
|
}
|
|
}
|
|
|
|
for (const auto& [count, timeword] : tokens) {
|
|
if (timeword.compare(std::string("seconds").substr(0, timeword.size())) == 0) {
|
|
tim += count;
|
|
} else if (timeword.compare("secs") == 0) {
|
|
tim += count;
|
|
} else if (timeword.compare(std::string("minutes").substr(0, timeword.size())) == 0) {
|
|
tim += count * 60;
|
|
} else if (timeword.compare("mins") == 0) {
|
|
tim += count * 60;
|
|
} else if (timeword.compare(std::string("hours").substr(0, timeword.size())) == 0) {
|
|
tim += count * 3600;
|
|
} else if (timeword.compare(std::string("days").substr(0, timeword.size())) == 0) {
|
|
tim += count * 86400;
|
|
} else if (timeword.compare(std::string("weeks").substr(0, timeword.size())) == 0) {
|
|
tim += count * 604800;
|
|
} else if (timeword.compare(std::string("months").substr(0, timeword.size())) == 0) {
|
|
tim += count * 2592000;
|
|
} else if (timeword.compare(std::string("years").substr(0, timeword.size())) == 0) {
|
|
tim += count * 31536000;
|
|
} else {
|
|
// Unrecognized timeword
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @brief Turn the given number of tics into a time.
|
|
*
|
|
* @param str String buffer to write into.
|
|
* @param time Number of tics to turn into a time.
|
|
* @param ceil Round up to the nearest second.
|
|
*/
|
|
void TicsToTime(OTimespan& span, int time, bool ceilsec)
|
|
{
|
|
if (time < 0)
|
|
{
|
|
// We do not support negative time, so just zero the struct.
|
|
span.hours = 0;
|
|
span.minutes = 0;
|
|
span.seconds = 0;
|
|
span.tics = 0;
|
|
span.csecs = 0;
|
|
return;
|
|
}
|
|
|
|
if (ceilsec)
|
|
{
|
|
if (time > 0)
|
|
{
|
|
// This ensures that if two clocks are run side by side and the
|
|
// normal time is exactly 1 second, the ceiling time is also 1
|
|
// second.
|
|
time -= 1;
|
|
}
|
|
|
|
time = time + TICRATE - (time % TICRATE);
|
|
}
|
|
|
|
span.hours = time / (TICRATE * 3600);
|
|
time -= span.hours * TICRATE * 3600;
|
|
|
|
span.minutes = time / (TICRATE * 60);
|
|
time -= span.minutes * TICRATE * 60;
|
|
|
|
span.seconds = time / TICRATE;
|
|
span.tics = time % TICRATE;
|
|
span.csecs = (span.tics * 100) / TICRATE;
|
|
}
|
|
|
|
// Trim whitespace from the start of a string
|
|
std::string &TrimStringStart(std::string &s)
|
|
{
|
|
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char c) { return !std::isspace(c); }));
|
|
return s;
|
|
}
|
|
|
|
// Trim whitespace from the end of a string
|
|
std::string &TrimStringEnd(std::string &s)
|
|
{
|
|
s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char c) { return !std::isspace(c); }).base(), s.end());
|
|
return s;
|
|
}
|
|
|
|
// Trim whitespace from the start and end of a string
|
|
std::string &TrimString(std::string &s)
|
|
{
|
|
return TrimStringStart(TrimStringEnd(s));
|
|
}
|
|
|
|
// Trim whitespace from the start of a string_view
|
|
std::string_view TrimStringViewStart(std::string_view str)
|
|
{
|
|
const auto it = std::find_if(str.begin(), str.end(), [](unsigned char c){
|
|
return !std::isspace(c);
|
|
});
|
|
|
|
str.remove_prefix(std::distance(str.begin(), it));
|
|
return str;
|
|
}
|
|
|
|
// Trim whitespace from the end of a string_view
|
|
std::string_view TrimStringViewEnd(std::string_view str)
|
|
{
|
|
const auto it = std::find_if(str.rbegin(), str.rend(), [](unsigned char c){
|
|
return !std::isspace(c);
|
|
});
|
|
|
|
str.remove_suffix(std::distance(str.rbegin(), it));
|
|
return str;
|
|
}
|
|
|
|
// Trim whitespace from the start and end of a string
|
|
std::string_view TrimStringView(std::string_view str)
|
|
{
|
|
return TrimStringViewStart(TrimStringViewEnd(str));
|
|
}
|
|
|
|
// Ensure that a string only has valid viewable ASCII in it.
|
|
bool ValidString(const std::string& s)
|
|
{
|
|
for (const auto c : s)
|
|
{
|
|
if (c < ' ' || c > '~')
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool IsHexString(const std::string& str, const size_t len)
|
|
{
|
|
if (str.length() != len)
|
|
return false;
|
|
|
|
for (const auto c : str)
|
|
{
|
|
if (c >= '0' && c <= '9')
|
|
continue;
|
|
if (c >= 'A' && c <= 'F')
|
|
continue;
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
//==========================================================================
|
|
//
|
|
// CheckWildcards
|
|
//
|
|
// [RH] Checks if text matches the wildcard pattern using ? or *
|
|
//
|
|
//==========================================================================
|
|
bool CheckWildcards (const char *pattern, const char *text)
|
|
{
|
|
if (pattern == NULL || text == NULL)
|
|
return true;
|
|
|
|
while (*pattern)
|
|
{
|
|
if (*pattern == '*')
|
|
{
|
|
char stop = tolower (*++pattern);
|
|
while (*text && tolower(*text) != stop)
|
|
{
|
|
text++;
|
|
}
|
|
if (*text && tolower(*text) == stop)
|
|
{
|
|
if (CheckWildcards (pattern, text++))
|
|
{
|
|
return true;
|
|
}
|
|
pattern--;
|
|
}
|
|
}
|
|
else if (*pattern == '?' || tolower(*pattern) == tolower(*text))
|
|
{
|
|
pattern++;
|
|
text++;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return (*pattern | *text) == 0;
|
|
}
|
|
|
|
class ReplacedStringTracker
|
|
{
|
|
typedef std::map<const char *, bool> replacedStrings_t;
|
|
typedef replacedStrings_t:: iterator iterator;
|
|
replacedStrings_t rs;
|
|
|
|
public:
|
|
|
|
void erase(const char *p)
|
|
{
|
|
iterator i = rs.find(p);
|
|
if(i != rs.end())
|
|
{
|
|
delete [] const_cast<char*>(i->first);
|
|
rs.erase(i);
|
|
}
|
|
}
|
|
void add(const char *p)
|
|
{
|
|
rs[p] = 1;
|
|
}
|
|
|
|
ReplacedStringTracker() : rs() {}
|
|
~ReplacedStringTracker()
|
|
{
|
|
for(iterator i = rs.begin(); i != rs.end(); ++i)
|
|
delete[] const_cast<char*>(i->first);
|
|
}
|
|
}rst;
|
|
|
|
void ReplaceString (char** ptr, const char *str)
|
|
{
|
|
if (*ptr)
|
|
{
|
|
if (*ptr == str)
|
|
return;
|
|
rst.erase(*ptr);
|
|
}
|
|
*ptr = copystring (str);
|
|
rst.add(*ptr);
|
|
}
|
|
|
|
|
|
//
|
|
// StripColorCodes
|
|
//
|
|
// Removes any color code markup from the given string.
|
|
//
|
|
void StripColorCodes(std::string& str)
|
|
{
|
|
size_t pos = 0;
|
|
while (pos < str.length())
|
|
{
|
|
if (str.c_str()[pos] == '\034' && str.c_str()[pos + 1] != '\0')
|
|
str.erase(pos, 2);
|
|
else
|
|
pos++;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Remap a value from one value range to another.
|
|
*
|
|
* @detail https://stackoverflow.com/q/3451553/91642
|
|
*
|
|
* @param value Value to remap.
|
|
* @param low1 Lower bound on the source range.
|
|
* @param high1 Upper bound on the source range.
|
|
* @param low2 Lower bound on the destination range.
|
|
* @param high2 Upper bound on the destination range.
|
|
*/
|
|
double Remap(const double value, const double low1, const double high1, const double low2,
|
|
const double high2)
|
|
{
|
|
return low2 + (value - low1) * (high2 - low2) / (high1 - low1);
|
|
}
|
|
|
|
//
|
|
// Log2
|
|
//
|
|
// Calculates the log base 2 of a 32-bit number using a lookup table.
|
|
//
|
|
// Based on public domain code written by Sean Eron Anderson.
|
|
// Taken from http://graphics.stanford.edu/~seander/bithacks.html
|
|
//
|
|
uint32_t Log2(uint32_t n)
|
|
{
|
|
#define LT(n) n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n
|
|
static constexpr signed char LogTable256[256] =
|
|
{
|
|
-1, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3,
|
|
LT(4), LT(5), LT(5), LT(6), LT(6), LT(6), LT(6),
|
|
LT(7), LT(7), LT(7), LT(7), LT(7), LT(7), LT(7), LT(7)
|
|
};
|
|
|
|
unsigned int t, tt; // temporaries
|
|
|
|
if ((tt = (n >> 16)))
|
|
return (t = (tt >> 8)) ? 24 + LogTable256[t] : 16 + LogTable256[tt];
|
|
else
|
|
return (t = (n >> 8)) ? 8 + LogTable256[t] : LogTable256[n];
|
|
}
|
|
|
|
VERSION_CONTROL (cmdlib_cpp, "$Id$")
|