mirror of
https://github.com/geode-sdk/geode.git
synced 2025-04-24 05:14:40 -04:00
remove log components, just format directly with fmtlib
This commit is contained in:
parent
d76c40e981
commit
c57db81910
8 changed files with 107 additions and 243 deletions
|
@ -81,7 +81,7 @@ if (PROJECT_IS_TOP_LEVEL AND NOT GEODE_BUILDING_DOCS)
|
|||
set(MAT_JSON_AS_INTERFACE ON)
|
||||
endif()
|
||||
CPMAddPackage("gh:geode-sdk/json#a47f570")
|
||||
CPMAddPackage("gh:fmtlib/fmt#9.1.0")
|
||||
CPMAddPackage("gh:fmtlib/fmt#10.1.1")
|
||||
CPMAddPackage("gh:gulrak/filesystem#3e5b930")
|
||||
|
||||
target_compile_definitions(${PROJECT_NAME} INTERFACE MAT_JSON_DYNAMIC=1)
|
||||
|
|
|
@ -12,156 +12,81 @@
|
|||
#include <vector>
|
||||
#include <span>
|
||||
|
||||
#include <fmt/core.h>
|
||||
|
||||
namespace cocos2d {
|
||||
GEODE_DLL std::string format_as(cocos2d::CCArray*);
|
||||
GEODE_DLL std::string format_as(cocos2d::ccColor3B const&);
|
||||
GEODE_DLL std::string format_as(cocos2d::ccColor4B const&);
|
||||
GEODE_DLL std::string format_as(cocos2d::ccColor4F const&);
|
||||
GEODE_DLL std::string format_as(cocos2d::CCNode*);
|
||||
GEODE_DLL std::string format_as(cocos2d::CCObject*);
|
||||
GEODE_DLL std::string format_as(cocos2d::CCPoint const&);
|
||||
GEODE_DLL std::string format_as(cocos2d::CCRect const&);
|
||||
GEODE_DLL std::string format_as(cocos2d::CCSize const&);
|
||||
}
|
||||
|
||||
namespace gd {
|
||||
inline std::string format_as(gd::string const& value) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
namespace ghc::filesystem {
|
||||
inline std::string format_as(ghc::filesystem::path const& value) {
|
||||
return value.string();
|
||||
}
|
||||
}
|
||||
|
||||
namespace geode {
|
||||
#pragma warning(disable : 4251)
|
||||
|
||||
class Mod;
|
||||
Mod* getMod();
|
||||
|
||||
GEODE_DLL std::string format_as(Mod*);
|
||||
|
||||
namespace log {
|
||||
using log_clock = std::chrono::system_clock;
|
||||
GEODE_DLL std::string generateLogName();
|
||||
|
||||
// Parse overloads
|
||||
GEODE_DLL std::string parse(cocos2d::CCArray*);
|
||||
GEODE_DLL std::string parse(cocos2d::ccColor3B const&);
|
||||
GEODE_DLL std::string parse(cocos2d::ccColor4B const&);
|
||||
GEODE_DLL std::string parse(cocos2d::ccColor4F const&);
|
||||
GEODE_DLL std::string parse(cocos2d::CCNode*);
|
||||
GEODE_DLL std::string parse(cocos2d::CCObject*);
|
||||
GEODE_DLL std::string parse(cocos2d::CCPoint const&);
|
||||
GEODE_DLL std::string parse(cocos2d::CCRect const&);
|
||||
GEODE_DLL std::string parse(cocos2d::CCSize const&);
|
||||
GEODE_DLL std::string parse(Mod*);
|
||||
GEODE_DLL std::string parse(gd::string const&);
|
||||
|
||||
template <class T>
|
||||
requires std::convertible_to<T*, cocos2d::CCNode*>
|
||||
std::string parse(T* node) {
|
||||
return parse(static_cast<cocos2d::CCNode*>(node));
|
||||
}
|
||||
template <class T>
|
||||
requires(
|
||||
std::convertible_to<T*, cocos2d::CCObject*> &&
|
||||
!std::convertible_to<T*, cocos2d::CCNode*>
|
||||
)
|
||||
std::string parse(T* node) {
|
||||
return parse(static_cast<cocos2d::CCObject*>(node));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires requires(T b) {
|
||||
std::stringstream() << b;
|
||||
}
|
||||
std::string parse(T const& thing) {
|
||||
std::stringstream buf;
|
||||
buf << thing;
|
||||
return buf.str();
|
||||
}
|
||||
|
||||
// todo: maybe add a debugParse function for these?
|
||||
|
||||
template <class T>
|
||||
requires requires(T t) {
|
||||
parse(t);
|
||||
}
|
||||
std::string parse(std::optional<T> const& thing) {
|
||||
if (thing.has_value()) {
|
||||
return "opt(" + parse(thing.value()) + ")";
|
||||
}
|
||||
return "nullopt";
|
||||
}
|
||||
|
||||
template <class T>
|
||||
requires requires(T t) {
|
||||
parse(t);
|
||||
}
|
||||
std::string parse(std::vector<T> const& thing) {
|
||||
std::string res = "[";
|
||||
bool first = true;
|
||||
for (auto& t : thing) {
|
||||
if (!first) {
|
||||
res += ", ";
|
||||
}
|
||||
first = false;
|
||||
res += parse(t);
|
||||
}
|
||||
res += "]";
|
||||
return res;
|
||||
}
|
||||
|
||||
template <class A, class B>
|
||||
requires requires(A a, B b) {
|
||||
parse(a);
|
||||
parse(b);
|
||||
}
|
||||
std::string parse(std::pair<A, B> const& thing) {
|
||||
return "(" + parse(thing.first) + ", " + parse(thing.second) + ")";
|
||||
}
|
||||
|
||||
template <class... T, std::size_t... Is>
|
||||
std::string parseTupleImpl(std::tuple<T...> const& tuple, std::index_sequence<Is...>) {
|
||||
std::string ret = "(";
|
||||
((ret += (Is == 0 ? "" : ", ") + parse(std::get<Is>(tuple))), ...);
|
||||
ret += ")";
|
||||
return ret;
|
||||
}
|
||||
|
||||
template <class... T>
|
||||
requires requires(T... t) {
|
||||
(parse(t), ...);
|
||||
}
|
||||
std::string parse(std::tuple<T...> const& tuple) {
|
||||
return parseTupleImpl(tuple, std::index_sequence_for<T...> {});
|
||||
}
|
||||
|
||||
// Log component system
|
||||
|
||||
struct GEODE_DLL ComponentTrait {
|
||||
virtual ~ComponentTrait() {}
|
||||
|
||||
virtual std::string _toString() = 0;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct ComponentBase : public ComponentTrait {
|
||||
T m_item;
|
||||
|
||||
inline ~ComponentBase() override {}
|
||||
|
||||
inline ComponentBase(T const& item) : m_item(item) {}
|
||||
|
||||
// specialization must implement
|
||||
inline std::string _toString() override {
|
||||
return parse(m_item);
|
||||
}
|
||||
};
|
||||
// template <class T>
|
||||
// requires std::convertible_to<T*, cocos2d::CCNode*>
|
||||
// std::string serialize(T* node) {
|
||||
// return serialize(static_cast<cocos2d::CCNode*>(node));
|
||||
// }
|
||||
// template <class T>
|
||||
// requires(
|
||||
// std::convertible_to<T*, cocos2d::CCObject*> &&
|
||||
// !std::convertible_to<T*, cocos2d::CCNode*>
|
||||
// )
|
||||
// std::string serialize(T* node) {
|
||||
// return serialize(static_cast<cocos2d::CCObject*>(node));
|
||||
// }
|
||||
|
||||
// Log
|
||||
|
||||
class GEODE_DLL Log final {
|
||||
Mod* m_sender;
|
||||
log_clock::time_point m_time;
|
||||
std::vector<ComponentTrait*> m_components;
|
||||
Severity m_severity;
|
||||
std::string m_content;
|
||||
|
||||
friend class Logger;
|
||||
public:
|
||||
~Log();
|
||||
Log(Mod* mod, Severity sev);
|
||||
Log(Severity sev, Mod* mod, std::string content);
|
||||
Log(Log&& l) = default;
|
||||
Log& operator=(Log&& l) = default;
|
||||
bool operator==(Log const& l);
|
||||
// bool operator==(Log const& l) const;
|
||||
|
||||
std::string toString(bool logTime = true) const;
|
||||
std::string toString(bool logTime, uint32_t nestLevel) const;
|
||||
|
||||
std::vector<ComponentTrait*>& getComponents();
|
||||
std::string const& getContent() const;
|
||||
log_clock::time_point getTime() const;
|
||||
Mod* getSender() const;
|
||||
Severity getSeverity() const;
|
||||
|
||||
Result<> addFormat(std::string_view formatStr, std::span<ComponentTrait*> comps);
|
||||
};
|
||||
|
||||
class GEODE_DLL Logger {
|
||||
|
@ -187,42 +112,31 @@ namespace geode {
|
|||
static void clear();
|
||||
};
|
||||
|
||||
GEODE_DLL void vlogImpl(Severity, Mod*, fmt::string_view format, fmt::format_args args);
|
||||
|
||||
template <typename... Args>
|
||||
requires requires(Args... b) {
|
||||
(parse(b), ...);
|
||||
}
|
||||
void internalLog(Severity sev, Mod* m, std::string_view formatStr, Args... args) {
|
||||
Log l(m, sev);
|
||||
|
||||
std::array<ComponentTrait*, sizeof...(Args)> comps = { static_cast<ComponentTrait*>(new ComponentBase(args))... };
|
||||
auto res = l.addFormat(formatStr, comps);
|
||||
|
||||
if (res.isErr()) {
|
||||
internalLog(Severity::Warning, getMod(), "Error parsing log format \"{}\": {}", formatStr, res.unwrapErr());
|
||||
return;
|
||||
}
|
||||
|
||||
Logger::push(std::move(l));
|
||||
inline void logImpl(Severity severity, Mod* mod, fmt::format_string<Args...> str, Args&&... args) {
|
||||
vlogImpl(severity, mod, str, fmt::make_format_args(args...));
|
||||
}
|
||||
|
||||
template <typename... Args>
|
||||
void debug(Args... args) {
|
||||
internalLog(Severity::Debug, getMod(), args...);
|
||||
inline void debug(fmt::format_string<Args...> str, Args&&... args) {
|
||||
logImpl(Severity::Debug, getMod(), str, std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
template <typename... Args>
|
||||
void info(Args... args) {
|
||||
internalLog(Severity::Info, getMod(), args...);
|
||||
inline void info(fmt::format_string<Args...> str, Args&&... args) {
|
||||
logImpl(Severity::Info, getMod(), str, std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
template <typename... Args>
|
||||
void warn(Args... args) {
|
||||
internalLog(Severity::Warning, getMod(), args...);
|
||||
inline void warn(fmt::format_string<Args...> str, Args&&... args) {
|
||||
logImpl(Severity::Warning, getMod(), str, std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
template <typename... Args>
|
||||
void error(Args... args) {
|
||||
internalLog(Severity::Error, getMod(), args...);
|
||||
inline void error(fmt::format_string<Args...> str, Args&&... args) {
|
||||
logImpl(Severity::Error, getMod(), str, std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
static void pushNest() {
|
||||
|
|
|
@ -179,8 +179,9 @@ namespace geode {
|
|||
}
|
||||
|
||||
std::string toString(bool includeTag = true) const;
|
||||
|
||||
friend GEODE_DLL std::string format_as(VersionInfo const& version);
|
||||
};
|
||||
GEODE_DLL std::ostream& operator<<(std::ostream& stream, VersionInfo const& version);
|
||||
|
||||
class GEODE_DLL ComparableVersionInfo final {
|
||||
protected:
|
||||
|
@ -225,8 +226,8 @@ namespace geode {
|
|||
}
|
||||
|
||||
std::string toString() const;
|
||||
friend GEODE_DLL std::string format_as(ComparableVersionInfo const& version);
|
||||
};
|
||||
GEODE_DLL std::ostream& operator<<(std::ostream& stream, ComparableVersionInfo const& version);
|
||||
}
|
||||
|
||||
template <class V>
|
||||
|
|
|
@ -455,7 +455,7 @@ void Loader::Impl::findProblems() {
|
|||
log::debug("{} is not enabled", id);
|
||||
continue;
|
||||
}
|
||||
log::debug(id);
|
||||
log::debug("{}", id);
|
||||
log::pushNest();
|
||||
|
||||
for (auto const& dep : mod->getMetadata().getDependencies()) {
|
||||
|
@ -688,7 +688,7 @@ bool Loader::Impl::loadHooks() {
|
|||
for (auto const& hook : m_uninitializedHooks) {
|
||||
auto res = hook.second->addHook(hook.first);
|
||||
if (!res) {
|
||||
log::internalLog(Severity::Error, hook.second, "{}", res.unwrapErr());
|
||||
log::logImpl(Severity::Error, hook.second, "{}", res.unwrapErr());
|
||||
hadErrors = true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ using namespace cocos2d;
|
|||
|
||||
// Parse overloads
|
||||
|
||||
std::string log::parse(Mod* mod) {
|
||||
std::string geode::format_as(Mod* mod) {
|
||||
if (mod) {
|
||||
return fmt::format("{{ Mod, {} }}", mod->getName());
|
||||
}
|
||||
|
@ -24,8 +24,9 @@ std::string log::parse(Mod* mod) {
|
|||
}
|
||||
}
|
||||
|
||||
std::string log::parse(CCObject* obj) {
|
||||
std::string cocos2d::format_as(CCObject* obj) {
|
||||
if (obj) {
|
||||
// TODO: try catch incase typeid fails
|
||||
return fmt::format("{{ {}, {} }}", typeid(*obj).name(), utils::intToHex(obj));
|
||||
}
|
||||
else {
|
||||
|
@ -33,7 +34,7 @@ std::string log::parse(CCObject* obj) {
|
|||
}
|
||||
}
|
||||
|
||||
std::string log::parse(CCNode* obj) {
|
||||
std::string cocos2d::format_as(CCNode* obj) {
|
||||
if (obj) {
|
||||
auto bb = obj->boundingBox();
|
||||
return fmt::format(
|
||||
|
@ -51,12 +52,12 @@ std::string log::parse(CCNode* obj) {
|
|||
}
|
||||
}
|
||||
|
||||
std::string log::parse(CCArray* arr) {
|
||||
std::string cocos2d::format_as(CCArray* arr) {
|
||||
std::string out = "[";
|
||||
|
||||
if (arr && arr->count()) {
|
||||
for (int i = 0; i < arr->count(); ++i) {
|
||||
out += parse(arr->objectAtIndex(i));
|
||||
out += format_as(arr->objectAtIndex(i));
|
||||
if (i < arr->count() - 1) out += ", ";
|
||||
}
|
||||
}
|
||||
|
@ -65,47 +66,52 @@ std::string log::parse(CCArray* arr) {
|
|||
return out + "]";
|
||||
}
|
||||
|
||||
std::string log::parse(CCPoint const& pt) {
|
||||
std::string cocos2d::format_as(CCPoint const& pt) {
|
||||
return fmt::format("{}, {}", pt.x, pt.y);
|
||||
}
|
||||
|
||||
std::string log::parse(CCSize const& sz) {
|
||||
std::string cocos2d::format_as(CCSize const& sz) {
|
||||
return fmt::format("{} : {}", sz.width, sz.height);
|
||||
}
|
||||
|
||||
std::string log::parse(CCRect const& rect) {
|
||||
return parse(rect.origin) + " | " + parse(rect.size);
|
||||
std::string cocos2d::format_as(CCRect const& rect) {
|
||||
return fmt::format("{} | {}", rect.origin, rect.size);
|
||||
}
|
||||
|
||||
std::string log::parse(cocos2d::ccColor3B const& col) {
|
||||
std::string cocos2d::format_as(cocos2d::ccColor3B const& col) {
|
||||
return fmt::format("rgb({}, {}, {})", col.r, col.g, col.b);
|
||||
}
|
||||
|
||||
std::string log::parse(cocos2d::ccColor4B const& col) {
|
||||
std::string cocos2d::format_as(cocos2d::ccColor4B const& col) {
|
||||
return fmt::format("rgba({}, {}, {}, {})", col.r, col.g, col.b, col.a);
|
||||
}
|
||||
|
||||
std::string log::parse(gd::string const& str) {
|
||||
return fmt::format("{}", std::string(str));
|
||||
}
|
||||
|
||||
// Log
|
||||
|
||||
Log::Log(Mod* mod, Severity sev) : m_sender(mod), m_time(log_clock::now()), m_severity(sev) {}
|
||||
void log::vlogImpl(Severity sev, Mod* mod, fmt::string_view format, fmt::format_args args) {
|
||||
Log obj(sev, mod, fmt::vformat(format, args));
|
||||
|
||||
Logger::push(std::move(obj));
|
||||
}
|
||||
|
||||
|
||||
Log::Log(Severity sev, Mod* mod, std::string content) :
|
||||
m_sender(mod),
|
||||
m_time(log_clock::now()),
|
||||
m_severity(sev),
|
||||
m_content(std::move(content)) {}
|
||||
|
||||
Log::~Log() {
|
||||
for (auto comp : m_components) {
|
||||
delete comp;
|
||||
}
|
||||
}
|
||||
|
||||
bool Log::operator==(Log const& l) {
|
||||
return this == &l;
|
||||
}
|
||||
// bool Log::operator==(Log const& l) {
|
||||
// return this == &l;
|
||||
// }
|
||||
|
||||
std::string Log::toString(bool logTime) const {
|
||||
return toString(logTime, 0);
|
||||
}
|
||||
|
||||
std::string Log::toString(bool logTime, uint32_t nestLevel) const {
|
||||
std::string res;
|
||||
|
||||
|
@ -149,17 +155,11 @@ std::string Log::toString(bool logTime, uint32_t nestLevel) const {
|
|||
res += " ";
|
||||
}
|
||||
|
||||
for (auto& i : m_components) {
|
||||
res += i->_toString();
|
||||
}
|
||||
res += m_content;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<ComponentTrait*>& Log::getComponents() {
|
||||
return m_components;
|
||||
}
|
||||
|
||||
log_clock::time_point Log::getTime() const {
|
||||
return m_time;
|
||||
}
|
||||
|
@ -172,59 +172,6 @@ Severity Log::getSeverity() const {
|
|||
return m_severity;
|
||||
}
|
||||
|
||||
Result<> Log::addFormat(std::string_view formatStr, std::span<ComponentTrait*> components) {
|
||||
size_t compIndex = 0;
|
||||
std::string current;
|
||||
for (size_t i = 0; i < formatStr.size(); ++i) {
|
||||
if (formatStr[i] == '{') {
|
||||
if (i == formatStr.size() - 1) {
|
||||
return Err("Unescaped { at the end of format string");
|
||||
}
|
||||
auto const next = formatStr[i + 1];
|
||||
if (next == '{') {
|
||||
current.push_back('{');
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
if (next == '}') {
|
||||
if (compIndex >= components.size()) {
|
||||
return Err("Not enough arguments for format string");
|
||||
}
|
||||
|
||||
m_components.push_back(new ComponentBase(current));
|
||||
m_components.push_back(components[compIndex++]);
|
||||
|
||||
current.clear();
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
return Err("You put something in between {} silly head");
|
||||
}
|
||||
if (formatStr[i] == '}') {
|
||||
if (i == formatStr.size() - 1) {
|
||||
return Err("Unescaped } at the end of format string");
|
||||
}
|
||||
if (formatStr[i + 1] == '}') {
|
||||
current.push_back('}');
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
return Err("You have an unescaped }");
|
||||
}
|
||||
|
||||
current.push_back(formatStr[i]);
|
||||
}
|
||||
|
||||
if (!current.empty())
|
||||
m_components.push_back(new ComponentBase(current));
|
||||
|
||||
if (compIndex != components.size()) {
|
||||
return Err("You have left over arguments.. silly head");
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
// Logger
|
||||
|
||||
std::vector<Log>& Logger::logs() {
|
||||
|
@ -256,7 +203,9 @@ void Logger::push(Log&& log) {
|
|||
}
|
||||
|
||||
void Logger::pop(Log* log) {
|
||||
geode::utils::ranges::remove(Logger::logs(), *log);
|
||||
geode::utils::ranges::remove(Logger::logs(), [&](auto& elem) {
|
||||
return &elem == log;
|
||||
});
|
||||
}
|
||||
|
||||
void Logger::pushNest() {
|
||||
|
|
|
@ -158,7 +158,7 @@ Result<> Mod::Impl::loadData() {
|
|||
if (auto setting = this->getSetting(key)) {
|
||||
// load its value
|
||||
if (!setting->load(value.json())) {
|
||||
log::internalLog(
|
||||
log::logImpl(
|
||||
Severity::Error,
|
||||
m_self,
|
||||
"{}: Unable to load value for setting \"{}\"",
|
||||
|
@ -168,7 +168,7 @@ Result<> Mod::Impl::loadData() {
|
|||
}
|
||||
}
|
||||
else {
|
||||
log::internalLog(
|
||||
log::logImpl(
|
||||
Severity::Warning,
|
||||
m_self,
|
||||
"Encountered unknown setting \"{}\" while loading "
|
||||
|
@ -215,7 +215,7 @@ Result<> Mod::Impl::saveData() {
|
|||
for (auto& [key, value] : m_settings) {
|
||||
coveredSettings.insert(key);
|
||||
if (!value->save(json[key])) {
|
||||
log::error("Unable to save setting \"" + key + "\"");
|
||||
log::error("Unable to save setting \"{}\"", key);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -320,7 +320,7 @@ struct MDParser {
|
|||
|
||||
default:
|
||||
{
|
||||
log::warn("Unhandled text type {}", type);
|
||||
log::warn("Unhandled text type {}", static_cast<int>(type));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -413,7 +413,7 @@ struct MDParser {
|
|||
|
||||
default:
|
||||
{
|
||||
log::warn("Unhandled block enter type {}", type);
|
||||
log::warn("Unhandled block enter type {}", static_cast<int>(type));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -516,7 +516,7 @@ struct MDParser {
|
|||
|
||||
default:
|
||||
{
|
||||
log::warn("Unhandled block leave type {}", type);
|
||||
log::warn("Unhandled block leave type {}", static_cast<int>(type));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -573,7 +573,7 @@ struct MDParser {
|
|||
|
||||
default:
|
||||
{
|
||||
log::warn("Unhandled span enter type {}", type);
|
||||
log::warn("Unhandled span enter type {}", static_cast<int>(type));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -627,7 +627,7 @@ struct MDParser {
|
|||
|
||||
default:
|
||||
{
|
||||
log::warn("Unhandled span leave type {}", type);
|
||||
log::warn("Unhandled span leave type {}", static_cast<int>(type));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -124,8 +124,8 @@ std::string VersionInfo::toString(bool includeTag) const {
|
|||
return fmt::format("v{}.{}.{}", m_major, m_minor, m_patch);
|
||||
}
|
||||
|
||||
std::ostream& geode::operator<<(std::ostream& stream, VersionInfo const& version) {
|
||||
return stream << version.toString();
|
||||
std::string geode::format_as(VersionInfo const& version) {
|
||||
return version.toString();
|
||||
}
|
||||
|
||||
// ComparableVersionInfo
|
||||
|
@ -179,6 +179,6 @@ std::string ComparableVersionInfo::toString() const {
|
|||
return prefix + m_version.toString();
|
||||
}
|
||||
|
||||
std::ostream& geode::operator<<(std::ostream& stream, ComparableVersionInfo const& version) {
|
||||
return stream << version.toString();
|
||||
std::string geode::format_as(ComparableVersionInfo const& version) {
|
||||
return version.toString();
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue