summaryrefslogtreecommitdiff
path: root/src/mmo
diff options
context:
space:
mode:
Diffstat (limited to 'src/mmo')
-rw-r--r--src/mmo/config_parse.cpp154
-rw-r--r--src/mmo/config_parse.hpp36
-rw-r--r--src/mmo/core.cpp125
-rw-r--r--src/mmo/core.hpp22
-rw-r--r--src/mmo/dumb_ptr.cpp3
-rw-r--r--src/mmo/dumb_ptr.hpp276
-rw-r--r--src/mmo/extract.cpp62
-rw-r--r--src/mmo/extract.hpp224
-rw-r--r--src/mmo/extract_test.cpp334
-rw-r--r--src/mmo/human_time_diff.cpp3
-rw-r--r--src/mmo/human_time_diff.hpp86
-rw-r--r--src/mmo/human_time_diff_test.cpp83
-rw-r--r--src/mmo/ip.cpp114
-rw-r--r--src/mmo/ip.hpp164
-rw-r--r--src/mmo/ip.py14
-rw-r--r--src/mmo/ip_test.cpp332
-rw-r--r--src/mmo/md5more.cpp128
-rw-r--r--src/mmo/md5more.hpp26
-rw-r--r--src/mmo/mmo.cpp3
-rw-r--r--src/mmo/mmo.hpp377
-rw-r--r--src/mmo/socket.cpp474
-rw-r--r--src/mmo/socket.hpp373
-rw-r--r--src/mmo/timer.cpp201
-rw-r--r--src/mmo/timer.hpp30
-rw-r--r--src/mmo/timer.t.hpp68
-rw-r--r--src/mmo/utils.cpp101
-rw-r--r--src/mmo/utils.hpp116
-rw-r--r--src/mmo/version.cpp55
-rw-r--r--src/mmo/version.hpp69
29 files changed, 4053 insertions, 0 deletions
diff --git a/src/mmo/config_parse.cpp b/src/mmo/config_parse.cpp
new file mode 100644
index 0000000..d677d8e
--- /dev/null
+++ b/src/mmo/config_parse.cpp
@@ -0,0 +1,154 @@
+#include "config_parse.hpp"
+// config_parse.hpp - Framework for per-server config parsers.
+//
+// Copyright © 2014 Ben Longbons <b.r.longbons@gmail.com>
+//
+// This file is part of The Mana World (Athena server)
+//
+// 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 3 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.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+#include "../strings/xstring.hpp"
+#include "../strings/zstring.hpp"
+
+#include "../io/cxxstdio.hpp"
+#include "../io/line.hpp"
+
+#include "version.hpp"
+
+#include "../poison.hpp"
+
+bool is_comment(XString line)
+{
+ return not line or line.startswith("//");
+}
+
+template<class ZS>
+inline
+bool config_split_impl(ZS line, XString *key, ZS *value)
+{
+ // unconditionally fail if line contains control characters
+ if (std::find_if(line.begin(), line.end(),
+ [](unsigned char c) { return c < ' '; }
+ ) != line.end())
+ return false;
+ // try to find a colon, fail if not found
+ typename ZS::iterator colon = std::find(line.begin(), line.end(), ':');
+ if (colon == line.end())
+ return false;
+
+ *key = line.xislice_h(colon).strip();
+ // move past the colon and any spaces
+ ++colon;
+ *value = line.xislice_t(colon).lstrip();
+ return true;
+}
+
+// eventually this should go away
+bool config_split(ZString line, XString *key, ZString *value)
+{
+ return config_split_impl(line, key, value);
+}
+// and use this instead
+bool config_split(XString line, XString *key, XString *value)
+{
+ return config_split_impl(line, key, value);
+}
+
+/// Master config parser. This handles 'import' and 'version-ge' etc.
+/// Then it defers to the inferior parser for a line it does not understand.
+bool load_config_file(ZString filename, ConfigItemParser slave)
+{
+ io::LineReader in(filename);
+ if (!in.is_open())
+ {
+ PRINTF("Unable to open file: %s\n", filename);
+ return false;
+ }
+ io::Line line;
+ bool rv = true;
+ while (in.read_line(line))
+ {
+ if (is_comment(line.text))
+ continue;
+ XString key;
+ ZString value;
+ if (!config_split(line.text, &key, &value))
+ {
+ line.error("Bad config line");
+ rv = false;
+ continue;
+ }
+ if (key == "import")
+ {
+ rv &= load_config_file(value, slave);
+ continue;
+ }
+ else if (key == "version-lt")
+ {
+ Version vers;
+ if (!extract(value, &vers))
+ {
+ rv = false;
+ continue;
+ }
+ if (CURRENT_VERSION < vers)
+ continue;
+ break;
+ }
+ else if (key == "version-le")
+ {
+ Version vers;
+ if (!extract(value, &vers))
+ {
+ rv = false;
+ continue;
+ }
+ if (CURRENT_VERSION <= vers)
+ continue;
+ break;
+ }
+ else if (key == "version-gt")
+ {
+ Version vers;
+ if (!extract(value, &vers))
+ {
+ rv = false;
+ continue;
+ }
+ if (CURRENT_VERSION > vers)
+ continue;
+ break;
+ }
+ else if (key == "version-ge")
+ {
+ Version vers;
+ if (!extract(value, &vers))
+ {
+ rv = false;
+ continue;
+ }
+ if (CURRENT_VERSION >= vers)
+ continue;
+ break;
+ }
+ else if (!slave(key, value))
+ {
+ line.error("Bad config key or value");
+ rv = false;
+ continue;
+ }
+ // nothing to see here, move along
+ }
+ return rv;
+}
diff --git a/src/mmo/config_parse.hpp b/src/mmo/config_parse.hpp
new file mode 100644
index 0000000..dd1b79e
--- /dev/null
+++ b/src/mmo/config_parse.hpp
@@ -0,0 +1,36 @@
+#ifndef TMWA_MMO_CONFIG_PARSE_HPP
+#define TMWA_MMO_CONFIG_PARSE_HPP
+// config_parse.hpp - Framework for per-server config parsers.
+//
+// Copyright © 2014 Ben Longbons <b.r.longbons@gmail.com>
+//
+// This file is part of The Mana World (Athena server)
+//
+// 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 3 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.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# include "../sanity.hpp"
+
+# include "../strings/fwd.hpp"
+
+typedef bool (*ConfigItemParser)(XString key, ZString value);
+
+bool is_comment(XString line);
+bool config_split(ZString line, XString *key, ZString *value);
+bool config_split(XString line, XString *key, XString *value);
+
+/// Master config parser. This handles 'import' and 'version-ge' etc.
+/// Then it defers to the inferior parser for a line it does not understand.
+bool load_config_file(ZString filename, ConfigItemParser slave);
+
+#endif // TMWA_MMO_CONFIG_PARSE_HPP
diff --git a/src/mmo/core.cpp b/src/mmo/core.cpp
new file mode 100644
index 0000000..1a9a52e
--- /dev/null
+++ b/src/mmo/core.cpp
@@ -0,0 +1,125 @@
+#include "core.hpp"
+
+#include <sys/wait.h>
+
+#include <unistd.h>
+
+#include <csignal>
+#include <cstdlib>
+#include <ctime>
+
+#include "../strings/zstring.hpp"
+
+#include "../generic/random.hpp"
+
+#include "../io/cxxstdio.hpp"
+
+#include "socket.hpp"
+#include "timer.hpp"
+
+#include "../poison.hpp"
+
+// Added by Gabuzomeu
+//
+// This is an implementation of signal() using sigaction() for portability.
+// (sigaction() is POSIX; signal() is not.) Taken from Stevens' _Advanced
+// Programming in the UNIX Environment_.
+//
+typedef void(*sigfunc)(int);
+static
+sigfunc compat_signal(int signo, sigfunc func)
+{
+ struct sigaction sact, oact;
+
+ sact.sa_handler = func;
+ sigfillset(&sact.sa_mask);
+ sigdelset(&sact.sa_mask, SIGSEGV);
+ sigdelset(&sact.sa_mask, SIGBUS);
+ sigdelset(&sact.sa_mask, SIGTRAP);
+ sigdelset(&sact.sa_mask, SIGILL);
+ sigdelset(&sact.sa_mask, SIGFPE);
+ sact.sa_flags = 0;
+
+ if (sigaction(signo, &sact, &oact) < 0)
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wold-style-cast"
+ return SIG_ERR;
+#pragma GCC diagnostic pop
+
+ return oact.sa_handler;
+}
+
+volatile
+bool runflag = true;
+
+static
+void chld_proc(int)
+{
+ wait(NULL);
+}
+static
+void sig_proc(int)
+{
+ for (int i = 1; i < 31; ++i)
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wold-style-cast"
+ compat_signal(i, SIG_IGN);
+#pragma GCC diagnostic pop
+ runflag = false;
+}
+
+/*
+ Note about fatal signals:
+
+ Under certain circumstances,
+ the following signals MUST not be ignored:
+ SIGFPE, SIGSEGV, SIGILL
+ Unless you use SA_SIGINFO and *carefully* check the origin,
+ that means they must be SIG_DFL.
+ */
+int main(int argc, char **argv)
+{
+ // ZString args[argc]; is (deliberately!) not supported by clang yet
+ ZString *args = static_cast<ZString *>(alloca(argc * sizeof(ZString)));
+ for (int i = 0; i < argc; ++i)
+ args[i] = ZString(strings::really_construct_from_a_pointer, argv[i], nullptr);
+ do_init(argc, args);
+
+ if (!runflag)
+ {
+ PRINTF("Fatal error during startup; exiting\n");
+ return 1;
+ }
+ // set up exit handlers *after* the initialization has happened.
+ // This is because term_func is likely to depend on successful init.
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wold-style-cast"
+ compat_signal(SIGPIPE, SIG_IGN);
+#pragma GCC diagnostic pop
+ compat_signal(SIGTERM, sig_proc);
+ compat_signal(SIGINT, sig_proc);
+ compat_signal(SIGCHLD, chld_proc);
+
+ // Signal to create coredumps by system when necessary (crash)
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wold-style-cast"
+ compat_signal(SIGSEGV, SIG_DFL);
+ compat_signal(SIGBUS, SIG_DFL);
+ compat_signal(SIGTRAP, SIG_DFL);
+ compat_signal(SIGILL, SIG_DFL);
+ compat_signal(SIGFPE, SIG_DFL);
+#pragma GCC diagnostic pop
+
+ atexit(term_func);
+
+ while (runflag)
+ {
+ // TODO - if timers take a long time to run, this
+ // may wait too long in sendrecv
+ tick_t now = milli_clock::now();
+ interval_t next = do_timer(now);
+ do_sendrecv(next);
+ do_parsepacket();
+ }
+}
diff --git a/src/mmo/core.hpp b/src/mmo/core.hpp
new file mode 100644
index 0000000..1788ece
--- /dev/null
+++ b/src/mmo/core.hpp
@@ -0,0 +1,22 @@
+#ifndef TMWA_MMO_CORE_HPP
+#define TMWA_MMO_CORE_HPP
+
+# include "../sanity.hpp"
+
+# include "../strings/fwd.hpp"
+
+/// core.c contains a server-independent main() function
+/// and then runs a do_sendrecv loop
+
+/// When this is cleared, the server exits gracefully.
+extern volatile bool runflag;
+
+/// This is an external function defined by each server
+/// This function must register stuff for the parse loop
+extern int do_init(int, ZString *);
+
+/// Cleanup function called whenever a signal kills us
+/// or when if we manage to exit() gracefully.
+extern void term_func(void);
+
+#endif // TMWA_MMO_CORE_HPP
diff --git a/src/mmo/dumb_ptr.cpp b/src/mmo/dumb_ptr.cpp
new file mode 100644
index 0000000..5f73d27
--- /dev/null
+++ b/src/mmo/dumb_ptr.cpp
@@ -0,0 +1,3 @@
+#include "dumb_ptr.hpp"
+
+#include "../poison.hpp"
diff --git a/src/mmo/dumb_ptr.hpp b/src/mmo/dumb_ptr.hpp
new file mode 100644
index 0000000..98c6308
--- /dev/null
+++ b/src/mmo/dumb_ptr.hpp
@@ -0,0 +1,276 @@
+#ifndef TMWA_MMO_DUMB_PTR_HPP
+#define TMWA_MMO_DUMB_PTR_HPP
+// ptr.hpp - temporary new/delete wrappers
+//
+// Copyright © 2013 Ben Longbons <b.r.longbons@gmail.com>
+//
+// This file is part of The Mana World (Athena server)
+//
+// 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 3 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.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# include "../sanity.hpp"
+
+# include <cstring>
+
+# include <algorithm>
+
+# include "../strings/astring.hpp"
+# include "../strings/zstring.hpp"
+# include "../strings/xstring.hpp"
+
+# include "../generic/const_array.hpp"
+
+// unmanaged new/delete-able pointer
+// should be replaced by std::unique_ptr<T>
+template<class T>
+class dumb_ptr
+{
+ template<class U>
+ friend class dumb_ptr;
+ T *impl;
+public:
+ explicit
+ dumb_ptr(T *p=nullptr)
+ : impl(p)
+ {}
+ template<class U>
+ dumb_ptr(dumb_ptr<U> p)
+ : impl(p.impl)
+ {}
+ dumb_ptr(std::nullptr_t)
+ : impl(nullptr)
+ {}
+
+ void delete_()
+ {
+ delete impl;
+ *this = nullptr;
+ }
+ template<class... A>
+ void new_(A&&... a)
+ {
+ impl = new T(std::forward<A>(a)...);
+ }
+ template<class... A>
+ static
+ dumb_ptr<T> make(A&&... a)
+ {
+ return dumb_ptr<T>(new T(std::forward<A>(a)...));
+ }
+ dumb_ptr& operator = (std::nullptr_t)
+ {
+ impl = nullptr;
+ return *this;
+ }
+
+ T& operator *() const
+ {
+ return *impl;
+ }
+ T *operator->() const
+ {
+ return impl;
+ }
+
+ explicit
+ operator bool() const
+ {
+ return impl;
+ }
+ bool operator !() const
+ {
+ return !impl;
+ }
+
+ friend bool operator == (dumb_ptr l, dumb_ptr r)
+ {
+ return l.impl == r.impl;
+ }
+ friend bool operator != (dumb_ptr l, dumb_ptr r)
+ {
+ return !(l == r);
+ }
+};
+
+// unmanaged new/delete-able pointer
+// should be replaced by std::unique_ptr<T[]> or std::vector<T>
+template<class T>
+class dumb_ptr<T[]>
+{
+ T *impl;
+ size_t sz;
+public:
+ dumb_ptr() : impl(), sz() {}
+ dumb_ptr(std::nullptr_t)
+ : impl(nullptr), sz(0) {}
+ dumb_ptr(T *p, size_t z)
+ : impl(p)
+ , sz(z)
+ {}
+
+ void delete_()
+ {
+ delete[] impl;
+ *this = nullptr;
+ }
+ void new_(size_t z)
+ {
+ impl = new T[z]();
+ sz = z;
+ }
+ static
+ dumb_ptr<T[]> make(size_t z)
+ {
+ return dumb_ptr<T[]>(new T[z](), z);
+ }
+ dumb_ptr& operator = (std::nullptr_t)
+ {
+ impl = nullptr;
+ sz = 0;
+ return *this;
+ }
+
+ size_t size() const
+ {
+ return sz;
+ }
+ void resize(size_t z)
+ {
+ if (z == sz)
+ return;
+ T *np = new T[z]();
+ // not exception-safe, but we don't have a dtor anyway
+ size_t i = std::min(z, sz);
+ while (i-->0)
+ np[i] = std::move(impl[i]);
+ delete[] impl;
+ impl = np;
+ sz = z;
+ }
+
+ T& operator[](size_t i) const
+ {
+ return impl[i];
+ }
+
+ explicit
+ operator bool() const
+ {
+ return impl;
+ }
+ bool operator !() const
+ {
+ return !impl;
+ }
+
+ friend bool operator == (dumb_ptr l, dumb_ptr r)
+ {
+ return l.impl == r.impl;
+ }
+ friend bool operator != (dumb_ptr l, dumb_ptr r)
+ {
+ return !(l == r);
+ }
+};
+
+struct dumb_string
+{
+ dumb_ptr<char[]> impl;
+
+ dumb_string()
+ : impl()
+ {}
+ dumb_string(char *) = delete;
+ // copy ctor, copy assign, and dtor are all default
+
+ static dumb_string copy(const char *b, const char *e)
+ {
+ dumb_string rv;
+ rv.impl.new_((e - b) + 1);
+ std::copy(b, e, &rv.impl[0]);
+ return rv;
+ }
+ static dumb_string copy(const char *sz)
+ {
+ return dumb_string::copy(sz, sz + strlen(sz));
+ }
+ static dumb_string copys(XString s)
+ {
+ return dumb_string::copy(&*s.begin(), &*s.end());
+ }
+ static
+# ifndef __clang__
+ __attribute__((warning("shouldn't use this - slice instead")))
+# endif
+ dumb_string copyn(const char *sn, size_t n)
+ {
+ return dumb_string::copy(sn, sn + strnlen(sn, n));
+ }
+
+ static
+ dumb_string fake(ZString p)
+ {
+ dumb_string rv;
+ size_t len = p.size();
+ rv.impl = dumb_ptr<char[]>(const_cast<char *>(p.c_str()), len);
+ return rv;
+ }
+
+ dumb_string dup() const
+ {
+ return dumb_string::copy(&impl[0]);
+ }
+ void delete_()
+ {
+ impl.delete_();
+ }
+
+ const char *c_str() const
+ {
+ return &impl[0];
+ }
+
+ operator ZString() const
+ {
+ return ZString(strings::really_construct_from_a_pointer, c_str(), nullptr);
+ }
+
+ AString str() const
+ {
+ return ZString(*this);
+ }
+
+ char& operator[](size_t i) const
+ {
+ return impl[i];
+ }
+
+ explicit
+ operator bool() const
+ {
+ return bool(impl);
+ }
+ bool operator !() const
+ {
+ return !impl;
+ }
+};
+
+inline
+const char *convert_for_printf(dumb_string ds)
+{
+ return ds.c_str();
+}
+
+#endif // TMWA_MMO_DUMB_PTR_HPP
diff --git a/src/mmo/extract.cpp b/src/mmo/extract.cpp
new file mode 100644
index 0000000..378986d
--- /dev/null
+++ b/src/mmo/extract.cpp
@@ -0,0 +1,62 @@
+#include "extract.hpp"
+// extract.cpp - a simple, hierarchical, tokenizer
+//
+// Copyright © 2013 Ben Longbons <b.r.longbons@gmail.com>
+//
+// This file is part of The Mana World (Athena server)
+//
+// 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 3 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.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+#include "../strings/astring.hpp"
+#include "../strings/xstring.hpp"
+#include "../strings/vstring.hpp"
+
+#include "../poison.hpp"
+
+bool extract(XString str, XString *rv)
+{
+ *rv = str;
+ return true;
+}
+
+bool extract(XString str, AString *rv)
+{
+ *rv = str;
+ return true;
+}
+
+bool extract(XString str, struct global_reg *var)
+{
+ return extract(str,
+ record<','>(&var->str, &var->value));
+}
+
+bool extract(XString str, struct item *it)
+{
+ XString ignored;
+ return extract(str,
+ record<',', 11>(
+ &it->id,
+ &it->nameid,
+ &it->amount,
+ &it->equip,
+ &ignored,
+ &ignored,
+ &ignored,
+ &ignored,
+ &ignored,
+ &ignored,
+ &ignored,
+ &ignored));
+}
diff --git a/src/mmo/extract.hpp b/src/mmo/extract.hpp
new file mode 100644
index 0000000..0ea9eb9
--- /dev/null
+++ b/src/mmo/extract.hpp
@@ -0,0 +1,224 @@
+#ifndef TMWA_MMO_EXTRACT_HPP
+#define TMWA_MMO_EXTRACT_HPP
+// extract.hpp - a simple, hierarchical, tokenizer
+//
+// Copyright © 2012-2013 Ben Longbons <b.r.longbons@gmail.com>
+//
+// This file is part of The Mana World (Athena server)
+//
+// 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 3 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.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# include "../sanity.hpp"
+
+# include <algorithm>
+
+# include "../strings/xstring.hpp"
+
+# include "../generic/const_array.hpp"
+
+# include "mmo.hpp"
+# include "utils.hpp"
+
+template<class T, typename=typename std::enable_if<std::is_integral<T>::value && !std::is_same<T, char>::value && !std::is_same<T, bool>::value>::type>
+bool extract(XString str, T *iv)
+{
+ if (!str || str.size() > 20)
+ return false;
+ if (!((str.front() == '-' && std::is_signed<T>::value)
+ || ('0' <= str.front() && str.front() <= '9')))
+ return false;
+ // needs a NUL, but can't always be given one. TODO VString?
+ char buf[20 + 1];
+ std::copy(str.begin(), str.end(), buf);
+ buf[str.size()] = '\0';
+
+ char *end;
+ errno = 0;
+ if (std::is_signed<T>::value)
+ {
+ long long v = strtoll(buf, &end, 10);
+ if (errno || *end)
+ return false;
+ *iv = v;
+ return *iv == v;
+ }
+ else
+ {
+ unsigned long long v = strtoull(buf, &end, 10);
+ if (errno || *end)
+ return false;
+ *iv = v;
+ return *iv == v;
+ }
+}
+
+inline
+bool extract(XString str, TimeT *tv)
+{
+ return extract(str, &tv->value);
+}
+
+// extra typename=void to workaround some duplicate overload rule
+template<class T, typename=typename std::enable_if<std::is_enum<T>::value>::type, typename=void>
+bool extract(XString str, T *iv)
+{
+ typedef typename underlying_type<T>::type U;
+ U v;
+ // defer to integer version
+ if (!extract(str, &v))
+ return false;
+ // TODO check bounds using enum min/max as in SSCANF
+ *iv = static_cast<T>(v);
+ return true;
+}
+
+bool extract(XString str, XString *rv);
+
+bool extract(XString str, AString *rv);
+
+template<uint8_t N>
+bool extract(XString str, VString<N> *out)
+{
+ if (str.size() > N)
+ return false;
+ *out = str;
+ return true;
+}
+
+template<class T>
+class LStripper
+{
+public:
+ T impl;
+};
+
+template<class T>
+LStripper<T> lstripping(T v)
+{
+ return {v};
+}
+
+template<class T>
+bool extract(XString str, LStripper<T> out)
+{
+ return extract(str.lstrip(), out.impl);
+}
+
+// basically just a std::tuple
+// but it provides its data members publically
+template<char split, int n, class... T>
+class Record;
+template<char split, int n>
+class Record<split, n>
+{
+};
+template<char split, int n, class F, class... R>
+class Record<split, n, F, R...>
+{
+public:
+ F frist;
+ Record<split, n - 1, R...> rest;
+public:
+ Record(F f, R... r)
+ : frist(f), rest(r...)
+ {}
+};
+template<char split, class... T>
+Record<split, sizeof...(T), T...> record(T... t)
+{
+ return Record<split, sizeof...(T), T...>(t...);
+}
+template<char split, int n, class... T>
+Record<split, n, T...> record(T... t)
+{
+ static_assert(0 < n && n < sizeof...(T), "don't be silly");
+ return Record<split, n, T...>(t...);
+}
+
+template<char split, int n>
+bool extract(XString str, Record<split, n>)
+{
+ return !str;
+}
+
+template<char split, int n, class F, class... R>
+bool extract(XString str, Record<split, n, F, R...> rec)
+{
+ XString::iterator s = std::find(str.begin(), str.end(), split);
+ XString::iterator s2 = s;
+ if (s2 != str.end())
+ ++s2;
+ XString head = str.xislice_h(s);
+ XString tail = str.xislice_t(s2);
+ if (s == str.end())
+ return (extract(head, rec.frist) && n <= 1)
+ || (!head && n <= 0);
+
+ return (extract(head, rec.frist) || n <= 0)
+ && extract(tail, rec.rest);
+}
+
+template<char split, class T>
+struct VRecord
+{
+ std::vector<T> *arr;
+};
+
+template<char split, class T>
+VRecord<split, T> vrec(std::vector<T> *arr)
+{
+ return {arr};
+}
+
+template<char split, class T>
+bool extract(XString str, VRecord<split, T> rec)
+{
+ if (!str)
+ return true;
+ XString::iterator s = std::find(str.begin(), str.end(), split);
+ rec.arr->emplace_back();
+ if (s == str.end())
+ return extract(str, &rec.arr->back());
+ return extract(str.xislice_h(s), &rec.arr->back())
+ && extract(str.xislice_t(s + 1), rec);
+}
+
+bool extract(XString str, struct global_reg *var);
+
+bool extract(XString str, struct item *it);
+
+inline
+bool extract(XString str, MapName *m)
+{
+ XString::iterator it = std::find(str.begin(), str.end(), '.');
+ str = str.xislice_h(it);
+ VString<15> tmp;
+ bool rv = extract(str, &tmp);
+ *m = tmp;
+ return rv;
+}
+
+inline
+bool extract(XString str, CharName *out)
+{
+ VString<23> tmp;
+ if (extract(str, &tmp))
+ {
+ *out = CharName(tmp);
+ return true;
+ }
+ return false;
+}
+
+#endif // TMWA_MMO_EXTRACT_HPP
diff --git a/src/mmo/extract_test.cpp b/src/mmo/extract_test.cpp
new file mode 100644
index 0000000..3d0610e
--- /dev/null
+++ b/src/mmo/extract_test.cpp
@@ -0,0 +1,334 @@
+#include "extract.hpp"
+
+#include <gtest/gtest.h>
+
+#include "../strings/xstring.hpp"
+
+TEST(extract, record_int)
+{
+ int x, y, z;
+ x = y = z = 0;
+ EXPECT_FALSE(extract("1 2 3 4 ", record<' '>(&x, &y, &z)));
+ EXPECT_EQ(1, x);
+ EXPECT_EQ(2, y);
+ EXPECT_EQ(3, z);
+ x = y = z = 0;
+ EXPECT_FALSE(extract("1 2 3 4", record<' '>(&x, &y, &z)));
+ EXPECT_EQ(1, x);
+ EXPECT_EQ(2, y);
+ EXPECT_EQ(3, z);
+ x = y = z = 0;
+ EXPECT_TRUE(extract("1 2 3 ", record<' '>(&x, &y, &z)));
+ EXPECT_EQ(1, x);
+ EXPECT_EQ(2, y);
+ EXPECT_EQ(3, z);
+ x = y = z = 0;
+ EXPECT_TRUE(extract("1 2 3", record<' '>(&x, &y, &z)));
+ EXPECT_EQ(1, x);
+ EXPECT_EQ(2, y);
+ EXPECT_EQ(3, z);
+ x = y = z = 0;
+ EXPECT_FALSE(extract("1 2 ", record<' '>(&x, &y, &z)));
+ EXPECT_EQ(1, x);
+ EXPECT_EQ(2, y);
+ EXPECT_EQ(0, z);
+ x = y = z = 0;
+ EXPECT_FALSE(extract("1 2", record<' '>(&x, &y, &z)));
+ EXPECT_EQ(1, x);
+ EXPECT_EQ(2, y);
+ EXPECT_EQ(0, z);
+ x = y = z = 0;
+ EXPECT_FALSE(extract("1 ", record<' '>(&x, &y, &z)));
+ EXPECT_EQ(1, x);
+ EXPECT_EQ(0, y);
+ EXPECT_EQ(0, z);
+ x = y = z = 0;
+ EXPECT_FALSE(extract("1", record<' '>(&x, &y, &z)));
+ EXPECT_EQ(1, x);
+ EXPECT_EQ(0, y);
+ EXPECT_EQ(0, z);
+ x = y = z = 0;
+ EXPECT_FALSE(extract(" ", record<' '>(&x, &y, &z)));
+ EXPECT_EQ(0, x);
+ EXPECT_EQ(0, y);
+ EXPECT_EQ(0, z);
+ x = y = z = 0;
+ EXPECT_FALSE(extract("", record<' '>(&x, &y, &z)));
+ EXPECT_EQ(0, x);
+ EXPECT_EQ(0, y);
+ EXPECT_EQ(0, z);
+ x = y = z = 0;
+
+ EXPECT_FALSE(extract("1 2 3 4 ", record<' ', 2>(&x, &y, &z)));
+ EXPECT_EQ(1, x);
+ EXPECT_EQ(2, y);
+ EXPECT_EQ(3, z);
+ x = y = z = 0;
+ EXPECT_FALSE(extract("1 2 3 4", record<' ', 2>(&x, &y, &z)));
+ EXPECT_EQ(1, x);
+ EXPECT_EQ(2, y);
+ EXPECT_EQ(3, z);
+ x = y = z = 0;
+ EXPECT_TRUE(extract("1 2 3 ", record<' ', 2>(&x, &y, &z)));
+ EXPECT_EQ(1, x);
+ EXPECT_EQ(2, y);
+ EXPECT_EQ(3, z);
+ x = y = z = 0;
+ EXPECT_TRUE(extract("1 2 3", record<' ', 2>(&x, &y, &z)));
+ EXPECT_EQ(1, x);
+ EXPECT_EQ(2, y);
+ EXPECT_EQ(3, z);
+ x = y = z = 0;
+ EXPECT_TRUE(extract("1 2 ", record<' ', 2>(&x, &y, &z)));
+ EXPECT_EQ(1, x);
+ EXPECT_EQ(2, y);
+ EXPECT_EQ(0, z);
+ x = y = z = 0;
+ EXPECT_TRUE(extract("1 2", record<' ', 2>(&x, &y, &z)));
+ EXPECT_EQ(1, x);
+ EXPECT_EQ(2, y);
+ EXPECT_EQ(0, z);
+ x = y = z = 0;
+ EXPECT_FALSE(extract("1 ", record<' ', 2>(&x, &y, &z)));
+ EXPECT_EQ(1, x);
+ EXPECT_EQ(0, y);
+ EXPECT_EQ(0, z);
+ x = y = z = 0;
+ EXPECT_FALSE(extract("1", record<' ', 2>(&x, &y, &z)));
+ EXPECT_EQ(1, x);
+ EXPECT_EQ(0, y);
+ EXPECT_EQ(0, z);
+ x = y = z = 0;
+ EXPECT_FALSE(extract(" ", record<' ', 2>(&x, &y, &z)));
+ EXPECT_EQ(0, x);
+ EXPECT_EQ(0, y);
+ EXPECT_EQ(0, z);
+ x = y = z = 0;
+ EXPECT_FALSE(extract("", record<' ', 2>(&x, &y, &z)));
+ EXPECT_EQ(0, x);
+ EXPECT_EQ(0, y);
+ EXPECT_EQ(0, z);
+ x = y = z = 0;
+
+ EXPECT_FALSE(extract("1 2 3 4 ", record<' ', 1>(&x, &y, &z)));
+ EXPECT_EQ(1, x);
+ EXPECT_EQ(2, y);
+ EXPECT_EQ(3, z);
+ x = y = z = 0;
+ EXPECT_FALSE(extract("1 2 3 4", record<' ', 1>(&x, &y, &z)));
+ EXPECT_EQ(1, x);
+ EXPECT_EQ(2, y);
+ EXPECT_EQ(3, z);
+ x = y = z = 0;
+ EXPECT_TRUE(extract("1 2 3 ", record<' ', 1>(&x, &y, &z)));
+ EXPECT_EQ(1, x);
+ EXPECT_EQ(2, y);
+ EXPECT_EQ(3, z);
+ x = y = z = 0;
+ EXPECT_TRUE(extract("1 2 3", record<' ', 1>(&x, &y, &z)));
+ EXPECT_EQ(1, x);
+ EXPECT_EQ(2, y);
+ EXPECT_EQ(3, z);
+ x = y = z = 0;
+ EXPECT_TRUE(extract("1 2 ", record<' ', 1>(&x, &y, &z)));
+ EXPECT_EQ(1, x);
+ EXPECT_EQ(2, y);
+ EXPECT_EQ(0, z);
+ x = y = z = 0;
+ EXPECT_TRUE(extract("1 2", record<' ', 1>(&x, &y, &z)));
+ EXPECT_EQ(1, x);
+ EXPECT_EQ(2, y);
+ EXPECT_EQ(0, z);
+ x = y = z = 0;
+ EXPECT_TRUE(extract("1 ", record<' ', 1>(&x, &y, &z)));
+ EXPECT_EQ(1, x);
+ EXPECT_EQ(0, y);
+ EXPECT_EQ(0, z);
+ x = y = z = 0;
+ EXPECT_TRUE(extract("1", record<' ', 1>(&x, &y, &z)));
+ EXPECT_EQ(1, x);
+ EXPECT_EQ(0, y);
+ EXPECT_EQ(0, z);
+ x = y = z = 0;
+ EXPECT_FALSE(extract(" ", record<' ', 1>(&x, &y, &z)));
+ EXPECT_EQ(0, x);
+ EXPECT_EQ(0, y);
+ EXPECT_EQ(0, z);
+ x = y = z = 0;
+ EXPECT_FALSE(extract("", record<' ', 1>(&x, &y, &z)));
+ EXPECT_EQ(0, x);
+ EXPECT_EQ(0, y);
+ EXPECT_EQ(0, z);
+ x = y = z = 0;
+}
+
+TEST(extract, record_str)
+{
+ XString x, y, z;
+ x = y = z = "";
+ EXPECT_FALSE(extract("1 2 3 4 ", record<' '>(&x, &y, &z)));
+ EXPECT_EQ("1", x);
+ EXPECT_EQ("2", y);
+ EXPECT_EQ("3", z);
+ x = y = z = "";
+ EXPECT_FALSE(extract("1 2 3 4", record<' '>(&x, &y, &z)));
+ EXPECT_EQ("1", x);
+ EXPECT_EQ("2", y);
+ EXPECT_EQ("3", z);
+ x = y = z = "";
+ EXPECT_TRUE(extract("1 2 3 ", record<' '>(&x, &y, &z)));
+ EXPECT_EQ("1", x);
+ EXPECT_EQ("2", y);
+ EXPECT_EQ("3", z);
+ x = y = z = "";
+ EXPECT_TRUE(extract("1 2 3", record<' '>(&x, &y, &z)));
+ EXPECT_EQ("1", x);
+ EXPECT_EQ("2", y);
+ EXPECT_EQ("3", z);
+ x = y = z = "";
+ EXPECT_TRUE(extract("1 2 ", record<' '>(&x, &y, &z)));
+ EXPECT_EQ("1", x);
+ EXPECT_EQ("2", y);
+ EXPECT_EQ("", z);
+ x = y = z = "";
+ EXPECT_FALSE(extract("1 2", record<' '>(&x, &y, &z)));
+ EXPECT_EQ("1", x);
+ EXPECT_EQ("2", y);
+ EXPECT_EQ("", z);
+ x = y = z = "";
+ EXPECT_FALSE(extract("1 ", record<' '>(&x, &y, &z)));
+ EXPECT_EQ("1", x);
+ EXPECT_EQ("", y);
+ EXPECT_EQ("", z);
+ x = y = z = "";
+ EXPECT_FALSE(extract("1", record<' '>(&x, &y, &z)));
+ EXPECT_EQ("1", x);
+ EXPECT_EQ("", y);
+ EXPECT_EQ("", z);
+ x = y = z = "";
+ EXPECT_FALSE(extract(" ", record<' '>(&x, &y, &z)));
+ EXPECT_EQ("", x);
+ EXPECT_EQ("", y);
+ EXPECT_EQ("", z);
+ x = y = z = "";
+ EXPECT_FALSE(extract("", record<' '>(&x, &y, &z)));
+ EXPECT_EQ("", x);
+ EXPECT_EQ("", y);
+ EXPECT_EQ("", z);
+ x = y = z = "";
+
+ EXPECT_FALSE(extract("1 2 3 4 ", record<' ', 2>(&x, &y, &z)));
+ EXPECT_EQ("1", x);
+ EXPECT_EQ("2", y);
+ EXPECT_EQ("3", z);
+ x = y = z = "";
+ EXPECT_FALSE(extract("1 2 3 4", record<' ', 2>(&x, &y, &z)));
+ EXPECT_EQ("1", x);
+ EXPECT_EQ("2", y);
+ EXPECT_EQ("3", z);
+ x = y = z = "";
+ EXPECT_TRUE(extract("1 2 3 ", record<' ', 2>(&x, &y, &z)));
+ EXPECT_EQ("1", x);
+ EXPECT_EQ("2", y);
+ EXPECT_EQ("3", z);
+ x = y = z = "";
+ EXPECT_TRUE(extract("1 2 3", record<' ', 2>(&x, &y, &z)));
+ EXPECT_EQ("1", x);
+ EXPECT_EQ("2", y);
+ EXPECT_EQ("3", z);
+ x = y = z = "";
+ EXPECT_TRUE(extract("1 2 ", record<' ', 2>(&x, &y, &z)));
+ EXPECT_EQ("1", x);
+ EXPECT_EQ("2", y);
+ EXPECT_EQ("", z);
+ x = y = z = "";
+ EXPECT_TRUE(extract("1 2", record<' ', 2>(&x, &y, &z)));
+ EXPECT_EQ("1", x);
+ EXPECT_EQ("2", y);
+ EXPECT_EQ("", z);
+ x = y = z = "";
+ EXPECT_TRUE(extract("1 ", record<' ', 2>(&x, &y, &z)));
+ EXPECT_EQ("1", x);
+ EXPECT_EQ("", y);
+ EXPECT_EQ("", z);
+ x = y = z = "";
+ EXPECT_FALSE(extract("1", record<' ', 2>(&x, &y, &z)));
+ EXPECT_EQ("1", x);
+ EXPECT_EQ("", y);
+ EXPECT_EQ("", z);
+ x = y = z = "";
+ EXPECT_TRUE(extract(" ", record<' ', 2>(&x, &y, &z)));
+ EXPECT_EQ("", x);
+ EXPECT_EQ("", y);
+ EXPECT_EQ("", z);
+ x = y = z = "";
+ EXPECT_FALSE(extract("", record<' ', 2>(&x, &y, &z)));
+ EXPECT_EQ("", x);
+ EXPECT_EQ("", y);
+ EXPECT_EQ("", z);
+ x = y = z = "";
+
+ EXPECT_FALSE(extract("1 2 3 4 ", record<' ', 1>(&x, &y, &z)));
+ EXPECT_EQ("1", x);
+ EXPECT_EQ("2", y);
+ EXPECT_EQ("3", z);
+ x = y = z = "";
+ EXPECT_FALSE(extract("1 2 3 4", record<' ', 1>(&x, &y, &z)));
+ EXPECT_EQ("1", x);
+ EXPECT_EQ("2", y);
+ EXPECT_EQ("3", z);
+ x = y = z = "";
+ EXPECT_TRUE(extract("1 2 3 ", record<' ', 1>(&x, &y, &z)));
+ EXPECT_EQ("1", x);
+ EXPECT_EQ("2", y);
+ EXPECT_EQ("3", z);
+ x = y = z = "";
+ EXPECT_TRUE(extract("1 2 3", record<' ', 1>(&x, &y, &z)));
+ EXPECT_EQ("1", x);
+ EXPECT_EQ("2", y);
+ EXPECT_EQ("3", z);
+ x = y = z = "";
+ EXPECT_TRUE(extract("1 2 ", record<' ', 1>(&x, &y, &z)));
+ EXPECT_EQ("1", x);
+ EXPECT_EQ("2", y);
+ EXPECT_EQ("", z);
+ x = y = z = "";
+ EXPECT_TRUE(extract("1 2", record<' ', 1>(&x, &y, &z)));
+ EXPECT_EQ("1", x);
+ EXPECT_EQ("2", y);
+ EXPECT_EQ("", z);
+ x = y = z = "";
+ EXPECT_TRUE(extract("1 ", record<' ', 1>(&x, &y, &z)));
+ EXPECT_EQ("1", x);
+ EXPECT_EQ("", y);
+ EXPECT_EQ("", z);
+ x = y = z = "";
+ EXPECT_TRUE(extract("1", record<' ', 1>(&x, &y, &z)));
+ EXPECT_EQ("1", x);
+ EXPECT_EQ("", y);
+ EXPECT_EQ("", z);
+ x = y = z = "";
+ EXPECT_TRUE(extract(" ", record<' ', 1>(&x, &y, &z)));
+ EXPECT_EQ("", x);
+ EXPECT_EQ("", y);
+ EXPECT_EQ("", z);
+ x = y = z = "";
+ EXPECT_TRUE(extract("", record<' ', 1>(&x, &y, &z)));
+ EXPECT_EQ("", x);
+ EXPECT_EQ("", y);
+ EXPECT_EQ("", z);
+ x = y = z = "";
+}
+
+TEST(extract, mapname)
+{
+ MapName map;
+ EXPECT_TRUE(extract("abc", &map));
+ EXPECT_EQ(map, "abc");
+ EXPECT_TRUE(extract("abc.gat", &map));
+ EXPECT_EQ(map, "abc");
+ EXPECT_TRUE(extract("abcdefghijklmno", &map));
+ EXPECT_EQ(map, "abcdefghijklmno");
+ EXPECT_TRUE(extract("abcdefghijklmno.gat", &map));
+ EXPECT_EQ(map, "abcdefghijklmno");
+}
diff --git a/src/mmo/human_time_diff.cpp b/src/mmo/human_time_diff.cpp
new file mode 100644
index 0000000..93a6d52
--- /dev/null
+++ b/src/mmo/human_time_diff.cpp
@@ -0,0 +1,3 @@
+#include "human_time_diff.hpp"
+
+#include "../poison.hpp"
diff --git a/src/mmo/human_time_diff.hpp b/src/mmo/human_time_diff.hpp
new file mode 100644
index 0000000..689b8d9
--- /dev/null
+++ b/src/mmo/human_time_diff.hpp
@@ -0,0 +1,86 @@
+#ifndef TMWA_MMO_HUMAN_TIME_DIFF_HPP
+#define TMWA_MMO_HUMAN_TIME_DIFF_HPP
+// human_time_diff.hpp - broken deltas
+//
+// Copyright © 2013 Ben Longbons <b.r.longbons@gmail.com>
+//
+// This file is part of The Mana World (Athena server)
+//
+// 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 3 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.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# include "../sanity.hpp"
+
+# include "../strings/xstring.hpp"
+
+# include "extract.hpp"
+
+struct HumanTimeDiff
+{
+ short year, month, day, hour, minute, second;
+
+ explicit
+ operator bool()
+ {
+ return year || month || day || hour || minute || second;
+ }
+
+ bool operator !()
+ {
+ return !bool(*this);
+ }
+};
+
+inline
+bool extract(XString str, HumanTimeDiff *iv)
+{
+ // str is a sequence of [-+]?[0-9]+([ay]|m|[jd]|h|mn|s)
+ // there are NO spaces here
+ // parse by counting the number starts
+ auto is_num = [](char c)
+ { return c == '-' || c == '+' || ('0' <= c && c <= '9'); };
+ if (!str || !is_num(str.front()))
+ return false;
+ *iv = HumanTimeDiff{};
+ while (str)
+ {
+ auto it = std::find_if_not(str.begin(), str.end(), is_num);
+ auto it2 = std::find_if(it, str.end(), is_num);
+ XString number = str.xislice_h(it);
+ XString suffix = str.xislice(it, it2);
+ str = str.xislice_t(it2);
+
+ short *ptr = nullptr;
+ if (suffix == "y" || suffix == "a")
+ ptr = &iv->year;
+ else if (suffix == "m")
+ ptr = &iv->month;
+ else if (suffix == "j" || suffix == "d")
+ ptr = &iv->day;
+ else if (suffix == "h")
+ ptr = &iv->hour;
+ else if (suffix == "mn")
+ ptr = &iv->minute;
+ else if (suffix == "s")
+ ptr = &iv->second;
+ else
+ return false;
+ if (number.startswith('+') && !number.startswith("+-"))
+ number = number.xslice_t(1);
+ if (*ptr || !extract(number, ptr))
+ return false;
+ }
+ return true;
+}
+
+#endif // TMWA_MMO_HUMAN_TIME_DIFF_HPP
diff --git a/src/mmo/human_time_diff_test.cpp b/src/mmo/human_time_diff_test.cpp
new file mode 100644
index 0000000..d3ddad1
--- /dev/null
+++ b/src/mmo/human_time_diff_test.cpp
@@ -0,0 +1,83 @@
+#include "human_time_diff.hpp"
+
+#include <gtest/gtest.h>
+
+// a sequence of [-+]?[0-9]+([ay]|m|[jd]|h|mn|s)
+
+TEST(humantimediff, single)
+{
+ HumanTimeDiff diff;
+
+ EXPECT_TRUE(extract("42y", &diff));
+ EXPECT_EQ(42, diff.year);
+ EXPECT_EQ(0, diff.month);
+ EXPECT_EQ(0, diff.day);
+ EXPECT_EQ(0, diff.hour);
+ EXPECT_EQ(0, diff.minute);
+ EXPECT_EQ(0, diff.second);
+
+ EXPECT_TRUE(extract("42m", &diff));
+ EXPECT_EQ(0, diff.year);
+ EXPECT_EQ(42, diff.month);
+ EXPECT_EQ(0, diff.day);
+ EXPECT_EQ(0, diff.hour);
+ EXPECT_EQ(0, diff.minute);
+ EXPECT_EQ(0, diff.second);
+
+ EXPECT_TRUE(extract("42d", &diff));
+ EXPECT_EQ(0, diff.year);
+ EXPECT_EQ(0, diff.month);
+ EXPECT_EQ(42, diff.day);
+ EXPECT_EQ(0, diff.hour);
+ EXPECT_EQ(0, diff.minute);
+ EXPECT_EQ(0, diff.second);
+
+ EXPECT_TRUE(extract("42h", &diff));
+ EXPECT_EQ(0, diff.year);
+ EXPECT_EQ(0, diff.month);
+ EXPECT_EQ(0, diff.day);
+ EXPECT_EQ(42, diff.hour);
+ EXPECT_EQ(0, diff.minute);
+ EXPECT_EQ(0, diff.second);
+
+ EXPECT_TRUE(extract("42mn", &diff));
+ EXPECT_EQ(0, diff.year);
+ EXPECT_EQ(0, diff.month);
+ EXPECT_EQ(0, diff.day);
+ EXPECT_EQ(0, diff.hour);
+ EXPECT_EQ(42, diff.minute);
+ EXPECT_EQ(0, diff.second);
+
+ EXPECT_TRUE(extract("42s", &diff));
+ EXPECT_EQ(0, diff.year);
+ EXPECT_EQ(0, diff.month);
+ EXPECT_EQ(0, diff.day);
+ EXPECT_EQ(0, diff.hour);
+ EXPECT_EQ(0, diff.minute);
+ EXPECT_EQ(42, diff.second);
+
+ EXPECT_TRUE(extract("+42y", &diff));
+ EXPECT_EQ(42, diff.year);
+ EXPECT_TRUE(extract("-42y", &diff));
+ EXPECT_EQ(-42, diff.year);
+ EXPECT_FALSE(extract("++42y", &diff));
+ EXPECT_FALSE(extract("+-42y", &diff));
+ EXPECT_FALSE(extract("-+42y", &diff));
+ EXPECT_FALSE(extract("--42y", &diff));
+ EXPECT_FALSE(extract("4+2y", &diff));
+ EXPECT_FALSE(extract("42z", &diff));
+}
+
+TEST(humantimediff, multiple)
+{
+ HumanTimeDiff diff;
+
+ EXPECT_TRUE(extract("42y23m-2d", &diff));
+ EXPECT_EQ(42, diff.year);
+ EXPECT_EQ(23, diff.month);
+ EXPECT_EQ(-2, diff.day);
+ EXPECT_EQ(0, diff.hour);
+ EXPECT_EQ(0, diff.minute);
+ EXPECT_EQ(0, diff.second);
+ EXPECT_FALSE(extract("1y2y", &diff));
+}
diff --git a/src/mmo/ip.cpp b/src/mmo/ip.cpp
new file mode 100644
index 0000000..146734a
--- /dev/null
+++ b/src/mmo/ip.cpp
@@ -0,0 +1,114 @@
+#include "ip.hpp"
+// ip.cpp - Implementation of IP address functions.
+//
+// Copyright © 2013 Ben Longbons <b.r.longbons@gmail.com>
+//
+// This file is part of The Mana World (Athena server)
+//
+// 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 3 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.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+#include "../strings/xstring.hpp"
+#include "../strings/vstring.hpp"
+
+#include "../io/cxxstdio.hpp"
+
+#include "../poison.hpp"
+
+bool extract(XString str, IP4Address *rv)
+{
+ if (str.endswith('.'))
+ return false;
+ uint8_t buf[4];
+ if (extract(str, record<'.'>(&buf[0], &buf[1], &buf[2], &buf[3])))
+ {
+ *rv = IP4Address(buf);
+ return true;
+ }
+ return false;
+}
+
+bool extract(XString str, IP4Mask *rv)
+{
+ IP4Address a, m;
+ unsigned b;
+ XString l, r;
+ if (str.endswith('/'))
+ return false;
+ if (extract(str, record<'/'>(&l, &r)))
+ {
+ // a.b.c.d/e.f.g.h or a.b.c.d/n
+ if (!extract(l, &a))
+ return false;
+ if (extract(r, &m))
+ {
+ *rv = IP4Mask(a, m);
+ return true;
+ }
+ if (!extract(r, &b) || b > 32)
+ return false;
+ }
+ else
+ {
+ // a.b.c.d or a.b.c.d. or a.b.c. or a.b. or a.
+ if (extract(str, &a))
+ {
+ *rv = IP4Mask(a, IP4_BROADCAST);
+ return true;
+ }
+ if (!str.endswith('.'))
+ return false;
+ uint8_t d[4] {};
+ if (extract(str, record<'.'>(&d[0], &d[1], &d[2], &d[3])))
+ b = 32;
+ else if (extract(str, record<'.'>(&d[0], &d[1], &d[2])))
+ b = 24;
+ else if (extract(str, record<'.'>(&d[0], &d[1])))
+ b = 16;
+ else if (extract(str, record<'.'>(&d[0])))
+ b = 8;
+ else
+ return false;
+ a = IP4Address(d);
+ }
+ // a is set; need to construct m from b
+ if (b == 0)
+ m = IP4Address();
+ else if (b == 32)
+ m = IP4_BROADCAST;
+ else
+ {
+ uint32_t s = -1;
+ s <<= (32 - b);
+ m = IP4Address({
+ static_cast<uint8_t>(s >> 24),
+ static_cast<uint8_t>(s >> 16),
+ static_cast<uint8_t>(s >> 8),
+ static_cast<uint8_t>(s >> 0),
+ });
+ }
+ *rv = IP4Mask(a, m);
+ return true;
+}
+
+VString<15> convert_for_printf(IP4Address a_)
+{
+ const uint8_t *a = a_.bytes();
+ return STRNPRINTF(16, "%hhu.%hhu.%hhu.%hhu", a[0], a[1], a[2], a[3]);
+}
+
+VString<31> convert_for_printf(IP4Mask a)
+{
+ return STRNPRINTF(32, "%s/%s",
+ a.addr(), a.mask());
+}
diff --git a/src/mmo/ip.hpp b/src/mmo/ip.hpp
new file mode 100644
index 0000000..a425710
--- /dev/null
+++ b/src/mmo/ip.hpp
@@ -0,0 +1,164 @@
+#ifndef TMWA_MMO_IP_HPP
+#define TMWA_MMO_IP_HPP
+// ip.hpp - classes to deal with IP addresses.
+//
+// Copyright © 2013 Ben Longbons <b.r.longbons@gmail.com>
+//
+// This file is part of The Mana World (Athena server)
+//
+// 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 3 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.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# include "../sanity.hpp"
+
+# include <netinet/in.h>
+
+# include "../strings/fwd.hpp"
+
+# include "extract.hpp"
+
+// TODO - in the long run ports belong here also
+// and of course, IPv6 stuff.
+// But what about unix socket addresses?
+
+/// Helper function
+template<class T, size_t n>
+constexpr
+bool _ce_a_lt(T (&a)[n], T (&b)[n], size_t i=0)
+{
+ return (i != n
+ && (a[i] < b[i]
+ || (a[i] == b[i]
+ && _ce_a_lt(a, b, i + 1))));
+}
+
+/// A 32-bit Ipv4 address. Does not include a port.
+/// Guaranteed to be laid out like the network wants.
+class IP4Address
+{
+ uint8_t _addr[4];
+public:
+ constexpr
+ IP4Address()
+ : _addr{}
+ {}
+ constexpr explicit
+ IP4Address(const uint8_t (&a)[4])
+ : _addr{a[0], a[1], a[2], a[3]}
+ {}
+ explicit
+ IP4Address(struct in_addr addr)
+ {
+ static_assert(sizeof(addr) == sizeof(_addr), "4 bytes");
+ *this = IP4Address(reinterpret_cast<const uint8_t (&)[4]>(addr));
+ }
+ explicit
+ operator struct in_addr() const
+ {
+ return reinterpret_cast<const struct in_addr&>(_addr);
+ }
+
+ constexpr friend
+ IP4Address operator & (IP4Address l, IP4Address r)
+ {
+ return IP4Address({
+ static_cast<uint8_t>(l._addr[0] & r._addr[0]),
+ static_cast<uint8_t>(l._addr[1] & r._addr[1]),
+ static_cast<uint8_t>(l._addr[2] & r._addr[2]),
+ static_cast<uint8_t>(l._addr[3] & r._addr[3]),
+ });
+ }
+
+ IP4Address& operator &= (IP4Address m)
+ { return *this = *this & m; }
+
+ const uint8_t *bytes() const
+ { return _addr; }
+
+ constexpr friend
+ bool operator < (IP4Address l, IP4Address r)
+ {
+ return _ce_a_lt(l._addr, r._addr);
+ }
+
+ constexpr friend
+ bool operator > (IP4Address l, IP4Address r)
+ {
+ return _ce_a_lt(r._addr, l._addr);
+ }
+
+ constexpr friend
+ bool operator >= (IP4Address l, IP4Address r)
+ {
+ return !_ce_a_lt(l._addr, r._addr);
+ }
+
+ constexpr friend
+ bool operator <= (IP4Address l, IP4Address r)
+ {
+ return !_ce_a_lt(r._addr, l._addr);
+ }
+
+ constexpr friend
+ bool operator == (IP4Address l, IP4Address r)
+ {
+ return !(l < r || r < l);
+ }
+
+ constexpr friend
+ bool operator != (IP4Address l, IP4Address r)
+ {
+ return (l < r || r < l);
+ }
+};
+
+class IP4Mask
+{
+ IP4Address _addr, _mask;
+public:
+ constexpr
+ IP4Mask() : _addr(), _mask()
+ {}
+ constexpr
+ IP4Mask(IP4Address a, IP4Address m) : _addr(a & m), _mask(m)
+ {}
+
+ constexpr
+ IP4Address addr() const
+ { return _addr; }
+ constexpr
+ IP4Address mask() const
+ { return _mask; }
+
+ constexpr
+ bool covers(IP4Address a) const
+ {
+ return (a & _mask) == _addr;
+ }
+};
+
+
+constexpr
+IP4Address IP4_LOCALHOST({127, 0, 0, 1});
+constexpr
+IP4Address IP4_BROADCAST({255, 255, 255, 255});
+
+
+VString<15> convert_for_printf(IP4Address a);
+VString<31> convert_for_printf(IP4Mask m);
+
+bool extract(XString str, IP4Address *iv);
+
+bool extract(XString str, IP4Mask *iv);
+
+#endif // TMWA_MMO_IP_HPP
diff --git a/src/mmo/ip.py b/src/mmo/ip.py
new file mode 100644
index 0000000..e6a8183
--- /dev/null
+++ b/src/mmo/ip.py
@@ -0,0 +1,14 @@
+class IP4Address(object):
+ ''' print an IP4Address
+ '''
+ __slots__ = ('_value')
+ name = 'IP4Address'
+ enabled = True
+
+ def __init__(self, value):
+ self._value = value
+
+ def to_string(self):
+ addr = self._value['_addr']
+ addr = tuple(int(addr[i]) for i in range(4))
+ return '%d.%d.%d.%d' % addr
diff --git a/src/mmo/ip_test.cpp b/src/mmo/ip_test.cpp
new file mode 100644
index 0000000..7ef1047
--- /dev/null
+++ b/src/mmo/ip_test.cpp
@@ -0,0 +1,332 @@
+#include "ip.hpp"
+
+#include <gtest/gtest.h>
+
+#include "../io/cxxstdio.hpp"
+
+#define CB(X) (std::integral_constant<bool, (X)>::value)
+TEST(ip4addr, cmp)
+{
+ constexpr static
+ IP4Address a = IP4_LOCALHOST;
+ constexpr static
+ IP4Address b = IP4_BROADCAST;
+
+ EXPECT_FALSE(CB(a < a));
+ EXPECT_TRUE (CB(a < b));
+ EXPECT_FALSE(CB(b < a));
+ EXPECT_FALSE(CB(b < b));
+
+ EXPECT_FALSE(CB(a > a));
+ EXPECT_FALSE(CB(a > b));
+ EXPECT_TRUE (CB(b > a));
+ EXPECT_FALSE(CB(b > b));
+
+ EXPECT_TRUE (CB(a <= a));
+ EXPECT_TRUE (CB(a <= b));
+ EXPECT_FALSE(CB(b <= a));
+ EXPECT_TRUE (CB(b <= b));
+
+ EXPECT_TRUE (CB(a >= a));
+ EXPECT_FALSE(CB(a >= b));
+ EXPECT_TRUE (CB(b >= a));
+ EXPECT_TRUE (CB(b >= b));
+
+ EXPECT_TRUE (CB(a == a));
+ EXPECT_FALSE(CB(a == b));
+ EXPECT_FALSE(CB(b == a));
+ EXPECT_TRUE (CB(b == b));
+
+ EXPECT_FALSE(CB(a != a));
+ EXPECT_TRUE (CB(a != b));
+ EXPECT_TRUE (CB(b != a));
+ EXPECT_FALSE(CB(b != b));
+}
+
+TEST(ip4addr, str)
+{
+ IP4Address a;
+ EXPECT_EQ("0.0.0.0", STRNPRINTF(17, "%s", a));
+ EXPECT_EQ("127.0.0.1", STRNPRINTF(17, "%s", IP4_LOCALHOST));
+ EXPECT_EQ("255.255.255.255", STRNPRINTF(17, "%s", IP4_BROADCAST));
+}
+
+TEST(ip4addr, extract)
+{
+ IP4Address a;
+ EXPECT_TRUE(extract("0.0.0.0", &a));
+ EXPECT_EQ("0.0.0.0", STRNPRINTF(16, "%s", a));
+ EXPECT_TRUE(extract("127.0.0.1", &a));
+ EXPECT_EQ("127.0.0.1", STRNPRINTF(16, "%s", a));
+ EXPECT_TRUE(extract("255.255.255.255", &a));
+ EXPECT_EQ("255.255.255.255", STRNPRINTF(16, "%s", a));
+ EXPECT_TRUE(extract("1.2.3.4", &a));
+ EXPECT_EQ("1.2.3.4", STRNPRINTF(16, "%s", a));
+
+ EXPECT_FALSE(extract("1.2.3.4.5", &a));
+ EXPECT_FALSE(extract("1.2.3.4.", &a));
+ EXPECT_FALSE(extract("1.2.3.", &a));
+ EXPECT_FALSE(extract("1.2.3", &a));
+ EXPECT_FALSE(extract("1.2.", &a));
+ EXPECT_FALSE(extract("1.2", &a));
+ EXPECT_FALSE(extract("1.", &a));
+ EXPECT_FALSE(extract("1", &a));
+ EXPECT_FALSE(extract("", &a));
+}
+
+
+TEST(ip4mask, body)
+{
+ IP4Mask m;
+ EXPECT_EQ(IP4Address(), m.addr());
+ EXPECT_EQ(IP4Address(), m.mask());
+ m = IP4Mask(IP4_LOCALHOST, IP4_BROADCAST);
+ EXPECT_EQ(IP4_LOCALHOST, m.addr());
+ EXPECT_EQ(IP4_BROADCAST, m.mask());
+}
+
+TEST(ip4mask, str)
+{
+ IP4Mask m;
+ EXPECT_EQ("0.0.0.0/0.0.0.0", STRNPRINTF(33, "%s", m));
+ m = IP4Mask(IP4_LOCALHOST, IP4_BROADCAST);
+ EXPECT_EQ("127.0.0.1/255.255.255.255", STRNPRINTF(33, "%s", m));
+}
+
+TEST(ip4mask, extract)
+{
+ IP4Mask m;
+ EXPECT_FALSE(extract("9.8.7.6/33", &m));
+ EXPECT_FALSE(extract("9.8.7.6.5", &m));
+ EXPECT_FALSE(extract("9.8.7.6/", &m));
+ EXPECT_FALSE(extract("9.8.7", &m));
+ EXPECT_FALSE(extract("9.8", &m));
+ EXPECT_FALSE(extract("9", &m));
+
+ EXPECT_TRUE(extract("127.0.0.1", &m));
+ EXPECT_EQ("127.0.0.1/255.255.255.255", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("127.0.0.1.", &m));
+ EXPECT_EQ("127.0.0.1/255.255.255.255", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("127.0.0.", &m));
+ EXPECT_EQ("127.0.0.0/255.255.255.0", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("127.0.", &m));
+ EXPECT_EQ("127.0.0.0/255.255.0.0", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("127.", &m));
+ EXPECT_EQ("127.0.0.0/255.0.0.0", STRNPRINTF(32, "%s", m));
+
+ EXPECT_TRUE(extract("1.2.3.4/255.255.255.255", &m));
+ EXPECT_EQ("1.2.3.4/255.255.255.255", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("1.2.3.0/255.255.255.0", &m));
+ EXPECT_EQ("1.2.3.0/255.255.255.0", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("1.2.0.4/255.255.0.255", &m));
+ EXPECT_EQ("1.2.0.4/255.255.0.255", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("1.2.0.0/255.255.0.0", &m));
+ EXPECT_EQ("1.2.0.0/255.255.0.0", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("1.0.3.4/255.0.255.255", &m));
+ EXPECT_EQ("1.0.3.4/255.0.255.255", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("1.0.3.0/255.0.255.0", &m));
+ EXPECT_EQ("1.0.3.0/255.0.255.0", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("1.0.0.4/255.0.0.255", &m));
+ EXPECT_EQ("1.0.0.4/255.0.0.255", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("1.0.0.0/255.0.0.0", &m));
+ EXPECT_EQ("1.0.0.0/255.0.0.0", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("0.2.3.4/0.255.255.255", &m));
+ EXPECT_EQ("0.2.3.4/0.255.255.255", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("0.2.3.0/0.255.255.0", &m));
+ EXPECT_EQ("0.2.3.0/0.255.255.0", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("0.2.0.4/0.255.0.255", &m));
+ EXPECT_EQ("0.2.0.4/0.255.0.255", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("0.2.0.0/0.255.0.0", &m));
+ EXPECT_EQ("0.2.0.0/0.255.0.0", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("0.0.3.4/0.0.255.255", &m));
+ EXPECT_EQ("0.0.3.4/0.0.255.255", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("0.0.3.0/0.0.255.0", &m));
+ EXPECT_EQ("0.0.3.0/0.0.255.0", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("0.0.0.4/0.0.0.255", &m));
+ EXPECT_EQ("0.0.0.4/0.0.0.255", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("0.0.0.0/0.0.0.0", &m));
+ EXPECT_EQ("0.0.0.0/0.0.0.0", STRNPRINTF(32, "%s", m));
+
+ // please don't do this
+ EXPECT_TRUE(extract("120.248.200.217/89.57.126.5", &m));
+ EXPECT_EQ("88.56.72.1/89.57.126.5", STRNPRINTF(32, "%s", m));
+
+ EXPECT_TRUE(extract("0.0.0.0/32", &m));
+ EXPECT_EQ("0.0.0.0/255.255.255.255", STRNPRINTF(32, "%s", m));
+
+ EXPECT_TRUE(extract("0.0.0.0/31", &m));
+ EXPECT_EQ("0.0.0.0/255.255.255.254", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("0.0.0.0/30", &m));
+ EXPECT_EQ("0.0.0.0/255.255.255.252", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("0.0.0.0/29", &m));
+ EXPECT_EQ("0.0.0.0/255.255.255.248", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("0.0.0.0/28", &m));
+ EXPECT_EQ("0.0.0.0/255.255.255.240", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("0.0.0.0/27", &m));
+ EXPECT_EQ("0.0.0.0/255.255.255.224", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("0.0.0.0/26", &m));
+ EXPECT_EQ("0.0.0.0/255.255.255.192", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("0.0.0.0/25", &m));
+ EXPECT_EQ("0.0.0.0/255.255.255.128", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("0.0.0.0/24", &m));
+ EXPECT_EQ("0.0.0.0/255.255.255.0", STRNPRINTF(32, "%s", m));
+
+ EXPECT_TRUE(extract("0.0.0.0/23", &m));
+ EXPECT_EQ("0.0.0.0/255.255.254.0", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("0.0.0.0/22", &m));
+ EXPECT_EQ("0.0.0.0/255.255.252.0", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("0.0.0.0/21", &m));
+ EXPECT_EQ("0.0.0.0/255.255.248.0", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("0.0.0.0/20", &m));
+ EXPECT_EQ("0.0.0.0/255.255.240.0", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("0.0.0.0/19", &m));
+ EXPECT_EQ("0.0.0.0/255.255.224.0", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("0.0.0.0/18", &m));
+ EXPECT_EQ("0.0.0.0/255.255.192.0", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("0.0.0.0/17", &m));
+ EXPECT_EQ("0.0.0.0/255.255.128.0", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("0.0.0.0/16", &m));
+ EXPECT_EQ("0.0.0.0/255.255.0.0", STRNPRINTF(32, "%s", m));
+
+ EXPECT_TRUE(extract("0.0.0.0/15", &m));
+ EXPECT_EQ("0.0.0.0/255.254.0.0", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("0.0.0.0/14", &m));
+ EXPECT_EQ("0.0.0.0/255.252.0.0", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("0.0.0.0/13", &m));
+ EXPECT_EQ("0.0.0.0/255.248.0.0", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("0.0.0.0/12", &m));
+ EXPECT_EQ("0.0.0.0/255.240.0.0", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("0.0.0.0/11", &m));
+ EXPECT_EQ("0.0.0.0/255.224.0.0", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("0.0.0.0/10", &m));
+ EXPECT_EQ("0.0.0.0/255.192.0.0", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("0.0.0.0/9", &m));
+ EXPECT_EQ("0.0.0.0/255.128.0.0", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("0.0.0.0/8", &m));
+ EXPECT_EQ("0.0.0.0/255.0.0.0", STRNPRINTF(32, "%s", m));
+
+ EXPECT_TRUE(extract("0.0.0.0/7", &m));
+ EXPECT_EQ("0.0.0.0/254.0.0.0", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("0.0.0.0/6", &m));
+ EXPECT_EQ("0.0.0.0/252.0.0.0", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("0.0.0.0/5", &m));
+ EXPECT_EQ("0.0.0.0/248.0.0.0", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("0.0.0.0/4", &m));
+ EXPECT_EQ("0.0.0.0/240.0.0.0", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("0.0.0.0/3", &m));
+ EXPECT_EQ("0.0.0.0/224.0.0.0", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("0.0.0.0/2", &m));
+ EXPECT_EQ("0.0.0.0/192.0.0.0", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("0.0.0.0/1", &m));
+ EXPECT_EQ("0.0.0.0/128.0.0.0", STRNPRINTF(32, "%s", m));
+ EXPECT_TRUE(extract("0.0.0.0/0", &m));
+ EXPECT_EQ("0.0.0.0/0.0.0.0", STRNPRINTF(32, "%s", m));
+}
+
+TEST(ip4mask, cover)
+{
+ IP4Address a;
+ IP4Address b = IP4_BROADCAST;
+ IP4Address l = IP4_LOCALHOST;
+ IP4Address h({127, 255, 255, 255});
+ IP4Address p24l({10, 0, 0, 0});
+ IP4Address p24h({10, 255, 255, 255});
+ IP4Address p20l({172, 16, 0, 0});
+ IP4Address p20h({172, 31, 255, 255});
+ IP4Address p16l({192, 168, 0, 0});
+ IP4Address p16h({192, 168, 255, 255});
+ IP4Mask m;
+ EXPECT_TRUE(m.covers(a));
+ EXPECT_TRUE(m.covers(b));
+ EXPECT_TRUE(m.covers(l));
+ EXPECT_TRUE(m.covers(h));
+ EXPECT_TRUE(m.covers(p24l));
+ EXPECT_TRUE(m.covers(p24h));
+ EXPECT_TRUE(m.covers(p20l));
+ EXPECT_TRUE(m.covers(p20h));
+ EXPECT_TRUE(m.covers(p16l));
+ EXPECT_TRUE(m.covers(p16h));
+ m = IP4Mask(l, a);
+ EXPECT_TRUE(m.covers(a));
+ EXPECT_TRUE(m.covers(b));
+ EXPECT_TRUE(m.covers(l));
+ EXPECT_TRUE(m.covers(h));
+ EXPECT_TRUE(m.covers(p24l));
+ EXPECT_TRUE(m.covers(p24h));
+ EXPECT_TRUE(m.covers(p20l));
+ EXPECT_TRUE(m.covers(p20h));
+ EXPECT_TRUE(m.covers(p16l));
+ EXPECT_TRUE(m.covers(p16h));
+ m = IP4Mask(l, b);
+ EXPECT_FALSE(m.covers(a));
+ EXPECT_FALSE(m.covers(b));
+ EXPECT_TRUE(m.covers(l));
+ EXPECT_FALSE(m.covers(h));
+ EXPECT_FALSE(m.covers(p24l));
+ EXPECT_FALSE(m.covers(p24h));
+ EXPECT_FALSE(m.covers(p20l));
+ EXPECT_FALSE(m.covers(p20h));
+ EXPECT_FALSE(m.covers(p16l));
+ EXPECT_FALSE(m.covers(p16h));
+
+ // but the really useful ones are with partial masks
+ m = IP4Mask(IP4Address({10, 0, 0, 0}), IP4Address({255, 0, 0, 0}));
+ EXPECT_FALSE(m.covers(a));
+ EXPECT_FALSE(m.covers(b));
+ EXPECT_FALSE(m.covers(l));
+ EXPECT_FALSE(m.covers(h));
+ EXPECT_TRUE(m.covers(p24l));
+ EXPECT_TRUE(m.covers(p24h));
+ EXPECT_FALSE(m.covers(p20l));
+ EXPECT_FALSE(m.covers(p20h));
+ EXPECT_FALSE(m.covers(p16l));
+ EXPECT_FALSE(m.covers(p16h));
+ EXPECT_FALSE(m.covers(IP4Address({9, 255, 255, 255})));
+ EXPECT_FALSE(m.covers(IP4Address({11, 0, 0, 0})));
+ m = IP4Mask(IP4Address({127, 0, 0, 0}), IP4Address({255, 0, 0, 0}));
+ EXPECT_FALSE(m.covers(a));
+ EXPECT_FALSE(m.covers(b));
+ EXPECT_TRUE(m.covers(l));
+ EXPECT_TRUE(m.covers(h));
+ EXPECT_FALSE(m.covers(p24l));
+ EXPECT_FALSE(m.covers(p24h));
+ EXPECT_FALSE(m.covers(p20l));
+ EXPECT_FALSE(m.covers(p20h));
+ EXPECT_FALSE(m.covers(p16l));
+ EXPECT_FALSE(m.covers(p16h));
+ EXPECT_FALSE(m.covers(IP4Address({126, 255, 255, 255})));
+ EXPECT_FALSE(m.covers(IP4Address({128, 0, 0, 0})));
+ m = IP4Mask(IP4Address({172, 16, 0, 0}), IP4Address({255, 240, 0, 0}));
+ EXPECT_FALSE(m.covers(a));
+ EXPECT_FALSE(m.covers(b));
+ EXPECT_FALSE(m.covers(l));
+ EXPECT_FALSE(m.covers(h));
+ EXPECT_FALSE(m.covers(p24l));
+ EXPECT_FALSE(m.covers(p24h));
+ EXPECT_TRUE(m.covers(p20l));
+ EXPECT_TRUE(m.covers(p20h));
+ EXPECT_FALSE(m.covers(p16l));
+ EXPECT_FALSE(m.covers(p16h));
+ EXPECT_FALSE(m.covers(IP4Address({172, 15, 255, 255})));
+ EXPECT_FALSE(m.covers(IP4Address({172, 32, 0, 0})));
+ m = IP4Mask(IP4Address({192, 168, 0, 0}), IP4Address({255, 255, 0, 0}));
+ EXPECT_FALSE(m.covers(a));
+ EXPECT_FALSE(m.covers(b));
+ EXPECT_FALSE(m.covers(l));
+ EXPECT_FALSE(m.covers(h));
+ EXPECT_FALSE(m.covers(p24l));
+ EXPECT_FALSE(m.covers(p24h));
+ EXPECT_FALSE(m.covers(p20l));
+ EXPECT_FALSE(m.covers(p20h));
+ EXPECT_TRUE(m.covers(p16l));
+ EXPECT_TRUE(m.covers(p16h));
+ EXPECT_FALSE(m.covers(IP4Address({192, 167, 255, 255})));
+ EXPECT_FALSE(m.covers(IP4Address({192, 169, 0, 0})));
+
+ // OTOH this is crazy
+ EXPECT_TRUE(extract("120.248.200.217/89.57.126.5", &m));
+ EXPECT_TRUE(m.covers(IP4Address({120, 248, 200, 217})));
+ EXPECT_TRUE(m.covers(IP4Address({88, 56, 72, 1})));
+ EXPECT_FALSE(m.covers(IP4Address({88, 56, 72, 0})));
+ EXPECT_FALSE(m.covers(IP4Address({88, 56, 72, 255})));
+}
diff --git a/src/mmo/md5more.cpp b/src/mmo/md5more.cpp
new file mode 100644
index 0000000..51ff5c4
--- /dev/null
+++ b/src/mmo/md5more.cpp
@@ -0,0 +1,128 @@
+#include "md5more.hpp"
+
+#include "../compat/rawmem.hpp"
+
+#include "../generic/random.hpp"
+
+#include "../io/cxxstdio.hpp"
+
+#include "../poison.hpp"
+
+#define X block.data
+
+// TODO - refactor MD5 into a stream, and merge the implementations
+// I once implemented an ostream that does it ...
+MD5_state MD5_from_FILE(io::ReadFile& in)
+{
+ uint64_t total_len = 0;
+
+ uint8_t buf[0x40];
+ uint8_t block_len = 0;
+
+ MD5_state state;
+ MD5_init(&state);
+
+ MD5_block block;
+
+ while (true)
+ {
+ size_t rv = in.get(sign_cast<char *>(buf + block_len), 0x40 - block_len);
+ if (!rv)
+ break;
+ total_len += 8 * rv; // in bits
+ block_len += rv;
+ if (block_len != 0x40)
+ continue;
+ for (int i = 0; i < 0x10; i++)
+ X[i] = buf[4 * i + 0] | buf[4 * i + 1] << 8 | buf[4 * i + 2] << 16 | buf[4 * i + 3] << 24;
+ MD5_do_block(&state, block);
+ block_len = 0;
+ }
+ // no more input, just pad and append the length
+ buf[block_len] = 0x80;
+ really_memset0(buf + block_len + 1, 0x40 - block_len - 1);
+ if (block_len < 0x38)
+ {
+ for (int i = 0; i < 8; i++)
+ buf[0x38 + i] = total_len >> i * 8;
+ }
+ for (int i = 0; i < 0x10; i++)
+ X[i] = buf[4 * i + 0] | buf[4 * i + 1] << 8 | buf[4 * i + 2] << 16 | buf[4 * i + 3] << 24;
+ MD5_do_block(&state, block);
+ if (0x38 <= block_len)
+ {
+ really_memset0(buf, 0x38);
+ for (int i = 0; i < 8; i++)
+ buf[0x38 + i] = total_len >> i * 8;
+ for (int i = 0; i < 0x10; i++)
+ X[i] = buf[4 * i + 0] | buf[4 * i + 1] << 8 | buf[4 * i + 2] << 16 | buf[4 * i + 3] << 24;
+ MD5_do_block(&state, block);
+ }
+ return state;
+}
+
+
+// Hash a password with a salt.
+// Whoever wrote this FAILS programming
+AccountCrypt MD5_saltcrypt(AccountPass key, SaltString salt)
+{
+ char cbuf[64] {};
+
+ // hash the key then the salt
+ // buf ends up as a 64-char NUL-terminated string
+ md5_string tbuf, tbuf2;
+ MD5_to_str(MD5_from_string(key), tbuf);
+ MD5_to_str(MD5_from_string(salt), tbuf2);
+ const auto it = std::copy(tbuf.begin(), tbuf.end(), std::begin(cbuf));
+ auto it2 = std::copy(tbuf2.begin(), tbuf2.end(), it);
+ assert(it2 == std::end(cbuf));
+
+ md5_string tbuf3;
+ MD5_to_str(MD5_from_string(XString(std::begin(cbuf), it2, nullptr)), tbuf3);
+
+ VString<31> obuf;
+
+ // This truncates the string, but we have to keep it like that for compatibility
+ SNPRINTF(obuf, 32, "!%s$%s", salt, tbuf3);
+ return stringish<AccountCrypt>(obuf);
+}
+
+SaltString make_salt(void)
+{
+ char salt[5];
+ for (int i = 0; i < 5; i++)
+ // 126 would probably actually be okay
+ salt[i] = random_::in(48, 125);
+ return stringish<SaltString>(XString(salt + 0, salt + 5, nullptr));
+}
+
+bool pass_ok(AccountPass password, AccountCrypt crypted)
+{
+ // crypted is like !salt$hash
+ auto begin = crypted.begin() + 1;
+ auto end = std::find(begin, crypted.end(), '$');
+ SaltString salt = stringish<SaltString>(crypted.xislice(begin, end));
+
+ return crypted == MD5_saltcrypt(password, salt);
+}
+
+// [M|h]ashes up an IP address and a secret key
+// to return a hopefully unique masked IP.
+IP4Address MD5_ip(IP4Address ip)
+{
+ static SaltString secret = make_salt();
+
+ // MD5sum a secret + the IP address
+ VString<31> ipbuf;
+ SNPRINTF(ipbuf, 32, "%s %s", ip, secret);
+ md5_binary obuf;
+ MD5_to_bin(MD5_from_string(ipbuf), obuf);
+
+ // Fold the md5sum to 32 bits, pack the bytes to an in_addr
+ return IP4Address({
+ static_cast<uint8_t>(obuf[0] ^ obuf[1] ^ obuf[8] ^ obuf[9]),
+ static_cast<uint8_t>(obuf[2] ^ obuf[3] ^ obuf[10] ^ obuf[11]),
+ static_cast<uint8_t>(obuf[4] ^ obuf[5] ^ obuf[12] ^ obuf[13]),
+ static_cast<uint8_t>(obuf[6] ^ obuf[7] ^ obuf[14] ^ obuf[15]),
+ });
+}
diff --git a/src/mmo/md5more.hpp b/src/mmo/md5more.hpp
new file mode 100644
index 0000000..0c50cca
--- /dev/null
+++ b/src/mmo/md5more.hpp
@@ -0,0 +1,26 @@
+#ifndef TMWA_MMO_MD5MORE_HPP
+#define TMWA_MMO_MD5MORE_HPP
+
+# include "../generic/md5.hpp"
+
+# include "../io/read.hpp"
+
+# include "ip.hpp"
+# include "mmo.hpp"
+
+MD5_state MD5_from_FILE(io::ReadFile& in);
+
+// whoever wrote this fails basic understanding of
+AccountCrypt MD5_saltcrypt(AccountPass key, SaltString salt);
+
+/// return some random characters
+// Currently, returns a 5-char string
+SaltString make_salt(void);
+
+/// check plaintext password against saved saltcrypt
+bool pass_ok(AccountPass password, AccountCrypt crypted);
+
+/// This returns an IP4Address because it is configurable whether it gets called at all
+IP4Address MD5_ip(IP4Address ip);
+
+#endif // TMWA_MMO_MD5MORE_HPP
diff --git a/src/mmo/mmo.cpp b/src/mmo/mmo.cpp
new file mode 100644
index 0000000..e9893ee
--- /dev/null
+++ b/src/mmo/mmo.cpp
@@ -0,0 +1,3 @@
+#include "mmo.hpp"
+
+#include "../poison.hpp"
diff --git a/src/mmo/mmo.hpp b/src/mmo/mmo.hpp
new file mode 100644
index 0000000..6b3cd53
--- /dev/null
+++ b/src/mmo/mmo.hpp
@@ -0,0 +1,377 @@
+/// Global structures and defines
+#ifndef TMWA_MMO_MMO_HPP
+#define TMWA_MMO_MMO_HPP
+
+# include "../sanity.hpp"
+
+# include "../compat/memory.hpp"
+
+# include "../strings/vstring.hpp"
+
+# include "../generic/enum.hpp"
+
+# include "timer.t.hpp"
+
+// affects CharName
+# define NAME_IGNORING_CASE 1
+
+constexpr int FIFOSIZE_SERVERLINK = 256 * 1024;
+
+constexpr int MAX_MAP_PER_SERVER = 512;
+constexpr int MAX_INVENTORY = 100;
+constexpr int MAX_AMOUNT = 30000;
+constexpr int MAX_ZENY = 1000000000; // 1G zeny
+
+enum class SkillID : uint16_t;
+constexpr SkillID MAX_SKILL = SkillID(474); // not 450
+constexpr SkillID get_enum_min_value(SkillID) { return SkillID(); }
+constexpr SkillID get_enum_max_value(SkillID) { return MAX_SKILL; }
+
+constexpr int GLOBAL_REG_NUM = 96;
+constexpr int ACCOUNT_REG_NUM = 16;
+constexpr int ACCOUNT_REG2_NUM = 16;
+constexpr interval_t DEFAULT_WALK_SPEED = std::chrono::milliseconds(150);
+constexpr interval_t MIN_WALK_SPEED = interval_t::zero();
+constexpr interval_t MAX_WALK_SPEED = std::chrono::seconds(1);
+constexpr int MAX_STORAGE = 300;
+constexpr int MAX_PARTY = 12;
+
+# define MIN_HAIR_STYLE battle_config.min_hair_style
+# define MAX_HAIR_STYLE battle_config.max_hair_style
+# define MIN_HAIR_COLOR battle_config.min_hair_color
+# define MAX_HAIR_COLOR battle_config.max_hair_color
+# define MIN_CLOTH_COLOR battle_config.min_cloth_color
+# define MAX_CLOTH_COLOR battle_config.max_cloth_color
+
+struct AccountName : VString<23> {};
+struct AccountPass : VString<23> {};
+struct AccountCrypt : VString<39> {};
+struct AccountEmail : VString<39> {};
+struct ServerName : VString<19> {};
+struct PartyName : VString<23> {};
+struct VarName : VString<31> {};
+
+# define DEFAULT_EMAIL stringish<AccountEmail>("a@a.com")
+
+// It is decreed: a mapname shall not contain an extension
+class MapName : public strings::_crtp_string<MapName, MapName, strings::ZPair>
+{
+ VString<15> _impl;
+public:
+ MapName() = default;
+ MapName(VString<15> v) : _impl(v.xislice_h(std::find(v.begin(), v.end(), '.'))) {}
+
+ iterator begin() const { return &*_impl.begin(); }
+ iterator end() const { return &*_impl.end(); }
+ const char *c_str() const { return _impl.c_str(); }
+
+ operator RString() const { return _impl; }
+ operator AString() const { return _impl; }
+ operator TString() const { return _impl; }
+ operator SString() const { return _impl; }
+ operator ZString() const { return _impl; }
+ operator XString() const { return _impl; }
+};
+template<>
+inline
+MapName stringish<MapName>(VString<15> iv)
+{
+ return iv;
+}
+inline
+const char *decay_for_printf(const MapName& vs) { return vs.c_str(); }
+
+// It is decreed: a charname is sometimes case sensitive
+struct CharName
+{
+private:
+ VString<23> _impl;
+public:
+ CharName() = default;
+ explicit CharName(VString<23> name)
+ : _impl(name)
+ {}
+
+ VString<23> to__actual() const
+ {
+ return _impl;
+ }
+ VString<23> to__lower() const
+ {
+ return _impl.to_lower();
+ }
+ VString<23> to__upper() const
+ {
+ return _impl.to_upper();
+ }
+ VString<23> to__canonical() const
+ {
+# if NAME_IGNORING_CASE == 0
+ return to__actual();
+# endif
+# if NAME_IGNORING_CASE == 1
+ return to__lower();
+# endif
+ }
+
+ friend bool operator == (const CharName& l, const CharName& r)
+ { return l.to__canonical() == r.to__canonical(); }
+ friend bool operator != (const CharName& l, const CharName& r)
+ { return l.to__canonical() != r.to__canonical(); }
+ friend bool operator < (const CharName& l, const CharName& r)
+ { return l.to__canonical() < r.to__canonical(); }
+ friend bool operator <= (const CharName& l, const CharName& r)
+ { return l.to__canonical() <= r.to__canonical(); }
+ friend bool operator > (const CharName& l, const CharName& r)
+ { return l.to__canonical() > r.to__canonical(); }
+ friend bool operator >= (const CharName& l, const CharName& r)
+ { return l.to__canonical() >= r.to__canonical(); }
+
+ friend
+ VString<23> convert_for_printf(const CharName& vs) { return vs.to__actual(); }
+};
+template<>
+inline
+CharName stringish<CharName>(VString<23> iv)
+{
+ return CharName(iv);
+}
+
+namespace e
+{
+enum class EPOS : uint16_t
+{
+ ZERO = 0x0000,
+
+ LEGS = 0x0001,
+ WEAPON = 0x0002,
+ GLOVES = 0x0004,
+ CAPE = 0x0008,
+ MISC1 = 0x0010,
+ SHIELD = 0x0020,
+ SHOES = 0x0040,
+ MISC2 = 0x0080,
+ HAT = 0x0100,
+ TORSO = 0x0200,
+
+ ARROW = 0x8000,
+};
+ENUM_BITWISE_OPERATORS(EPOS)
+
+constexpr EPOS get_enum_min_value(EPOS) { return EPOS(0x0000); }
+constexpr EPOS get_enum_max_value(EPOS) { return EPOS(0xffff); }
+}
+using e::EPOS;
+
+struct item
+{
+ int id;
+ short nameid;
+ short amount;
+ EPOS equip;
+};
+
+struct point
+{
+ MapName map_;
+ short x, y;
+};
+
+namespace e
+{
+enum class SkillFlags : uint16_t;
+}
+using e::SkillFlags;
+
+struct skill_value
+{
+ unsigned short lv;
+ SkillFlags flags;
+
+ friend bool operator == (const skill_value& l, const skill_value& r)
+ {
+ return l.lv == r.lv && l.flags == r.flags;
+ }
+ friend bool operator != (const skill_value& l, const skill_value& r)
+ {
+ return !(l == r);
+ }
+};
+
+struct global_reg
+{
+ VarName str;
+ int value;
+};
+
+// Option and Opt1..3 in map.hpp
+namespace e
+{
+enum class Option : uint16_t;
+constexpr Option get_enum_min_value(Option) { return Option(0x0000); }
+constexpr Option get_enum_max_value(Option) { return Option(0xffff); }
+}
+using e::Option;
+
+enum class ATTR
+{
+ STR = 0,
+ AGI = 1,
+ VIT = 2,
+ INT = 3,
+ DEX = 4,
+ LUK = 5,
+
+ COUNT = 6,
+};
+
+constexpr ATTR ATTRs[6] =
+{
+ ATTR::STR,
+ ATTR::AGI,
+ ATTR::VIT,
+ ATTR::INT,
+ ATTR::DEX,
+ ATTR::LUK,
+};
+
+enum class ItemLook : uint16_t
+{
+ NONE = 0,
+ BLADE = 1, // or some other common weapons
+ _2,
+ SETZER_AND_SCYTHE = 3,
+ _6,
+ STAFF = 10,
+ BOW = 11,
+ _13 = 13,
+ _14 = 14,
+ _16 = 16,
+ SINGLE_HANDED_COUNT = 17,
+
+ DUAL_BLADE = 0x11,
+ DUAL_2 = 0x12,
+ DUAL_6 = 0x13,
+ DUAL_12 = 0x14,
+ DUAL_16 = 0x15,
+ DUAL_26 = 0x16,
+};
+
+enum class SEX : uint8_t
+{
+ FEMALE = 0,
+ MALE = 1,
+ // For items. This is also used as error, sometime.
+ NEUTRAL = 2,
+};
+inline
+char sex_to_char(SEX sex)
+{
+ switch (sex)
+ {
+ case SEX::FEMALE: return 'F';
+ case SEX::MALE: return 'M';
+ default: return '\0';
+ }
+}
+inline
+SEX sex_from_char(char c)
+{
+ switch (c)
+ {
+ case 'F': return SEX::FEMALE;
+ case 'M': return SEX::MALE;
+ default: return SEX::NEUTRAL;
+ }
+}
+
+struct CharKey
+{
+ CharName name;
+ int account_id;
+ int char_id;
+ unsigned char char_num;
+};
+
+struct CharData
+{
+ int partner_id;
+
+ int base_exp, job_exp, zeny;
+
+ short species;
+ short status_point, skill_point;
+ int hp, max_hp, sp, max_sp;
+ Option option;
+ short karma, manner;
+ short hair, hair_color, clothes_color;
+ int party_id;
+
+ ItemLook weapon;
+ short shield;
+ short head_top, head_mid, head_bottom;
+
+ unsigned char base_level, job_level;
+ earray<short, ATTR, ATTR::COUNT> attrs;
+ SEX sex;
+
+ unsigned long mapip;
+ unsigned int mapport;
+
+ struct point last_point, save_point;
+ struct item inventory[MAX_INVENTORY];
+ earray<skill_value, SkillID, MAX_SKILL> skill;
+ int global_reg_num;
+ struct global_reg global_reg[GLOBAL_REG_NUM];
+ int account_reg_num;
+ struct global_reg account_reg[ACCOUNT_REG_NUM];
+ int account_reg2_num;
+ struct global_reg account_reg2[ACCOUNT_REG2_NUM];
+};
+
+struct CharPair
+{
+ CharKey key;
+ std::unique_ptr<CharData> data;
+
+ CharPair()
+ : key{}, data(make_unique<CharData>())
+ {}
+};
+
+struct storage
+{
+ int dirty;
+ int account_id;
+ short storage_status;
+ short storage_amount;
+ struct item storage_[MAX_STORAGE];
+};
+
+//struct map_session_data;
+
+struct GM_Account
+{
+ int account_id;
+ uint8_t level;
+};
+
+struct party_member
+{
+ int account_id;
+ CharName name;
+ MapName map;
+ int leader, online, lv;
+ struct map_session_data *sd;
+};
+
+struct party
+{
+ int party_id;
+ PartyName name;
+ int exp;
+ int item;
+ struct party_member member[MAX_PARTY];
+};
+
+#endif // TMWA_MMO_MMO_HPP
diff --git a/src/mmo/socket.cpp b/src/mmo/socket.cpp
new file mode 100644
index 0000000..73e32a4
--- /dev/null
+++ b/src/mmo/socket.cpp
@@ -0,0 +1,474 @@
+#include "socket.hpp"
+
+#include <arpa/inet.h>
+#include <netinet/tcp.h>
+#include <sys/socket.h>
+//#include <sys/types.h>
+
+#include <fcntl.h>
+#include <unistd.h>
+
+#include <cstdlib>
+#include <cstring>
+#include <ctime>
+
+#include "../io/cxxstdio.hpp"
+#include "core.hpp"
+#include "timer.hpp"
+#include "utils.hpp"
+
+#include "../poison.hpp"
+
+static
+io::FD_Set readfds;
+static
+int fd_max;
+
+static
+const uint32_t RFIFO_SIZE = 65536;
+static
+const uint32_t WFIFO_SIZE = 65536;
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wold-style-cast"
+static
+std::array<std::unique_ptr<Session>, FD_SETSIZE> session;
+#pragma GCC diagnostic pop
+
+void set_session(io::FD fd, std::unique_ptr<Session> sess)
+{
+ int f = fd.uncast_dammit();
+ assert (0 <= f && f < FD_SETSIZE);
+ session[f] = std::move(sess);
+}
+Session *get_session(io::FD fd)
+{
+ int f = fd.uncast_dammit();
+ if (0 <= f && f < FD_SETSIZE)
+ return session[f].get();
+ return nullptr;
+}
+void reset_session(io::FD fd)
+{
+ int f = fd.uncast_dammit();
+ assert (0 <= f && f < FD_SETSIZE);
+ session[f] = nullptr;
+}
+int get_fd_max() { return fd_max; }
+IteratorPair<ValueIterator<io::FD, IncrFD>> iter_fds()
+{
+ return {io::FD::cast_dammit(0), io::FD::cast_dammit(fd_max)};
+}
+
+/// clean up by discarding handled bytes
+inline
+void RFIFOFLUSH(Session *s)
+{
+ really_memmove(&s->rdata[0], &s->rdata[s->rdata_pos], RFIFOREST(s));
+ s->rdata_size = RFIFOREST(s);
+ s->rdata_pos = 0;
+}
+
+/// how much room there is to read more data
+inline
+size_t RFIFOSPACE(Session *s)
+{
+ return s->max_rdata - s->rdata_size;
+}
+
+
+/// Discard all input
+static
+void null_parse(Session *s);
+/// Default parser for new connections
+static
+void (*default_func_parse)(Session *) = null_parse;
+
+void set_defaultparse(void (*defaultparse)(Session *))
+{
+ default_func_parse = defaultparse;
+}
+
+/// Read from socket to the queue
+static
+void recv_to_fifo(Session *s)
+{
+ if (s->eof)
+ return;
+
+ ssize_t len = s->fd.read(&s->rdata[s->rdata_size],
+ RFIFOSPACE(s));
+
+ if (len > 0)
+ {
+ s->rdata_size += len;
+ s->connected = 1;
+ }
+ else
+ {
+ s->eof = 1;
+ }
+}
+
+static
+void send_from_fifo(Session *s)
+{
+ if (s->eof)
+ return;
+
+ ssize_t len = s->fd.write(&s->wdata[0], s->wdata_size);
+
+ if (len > 0)
+ {
+ s->wdata_size -= len;
+ if (s->wdata_size)
+ {
+ really_memmove(&s->wdata[0], &s->wdata[len],
+ s->wdata_size);
+ }
+ s->connected = 1;
+ }
+ else
+ {
+ s->eof = 1;
+ }
+}
+
+static
+void null_parse(Session *s)
+{
+ PRINTF("null_parse : %d\n", s);
+ RFIFOSKIP(s, RFIFOREST(s));
+}
+
+
+static
+void connect_client(Session *ls)
+{
+ struct sockaddr_in client_address;
+ socklen_t len = sizeof(client_address);
+
+ io::FD fd = ls->fd.accept(reinterpret_cast<struct sockaddr *>(&client_address), &len);
+ if (fd == io::FD())
+ {
+ perror("accept");
+ return;
+ }
+ if (fd.uncast_dammit() >= SOFT_LIMIT)
+ {
+ FPRINTF(stderr, "softlimit reached, disconnecting : %d\n", fd.uncast_dammit());
+ fd.shutdown(SHUT_RDWR);
+ fd.close();
+ return;
+ }
+ if (fd_max <= fd.uncast_dammit())
+ {
+ fd_max = fd.uncast_dammit() + 1;
+ }
+
+ const int yes = 1;
+ /// Allow to bind() again after the server restarts.
+ // Since the socket is still in the TIME_WAIT, there's a possibility
+ // that formerly lost packets might be delivered and confuse the server.
+ fd.setsockopt(SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes);
+ /// Send packets as soon as possible
+ /// even if the kernel thinks there is too little for it to be worth it!
+ /// Testing shows this is indeed a good idea.
+ fd.setsockopt(IPPROTO_TCP, TCP_NODELAY, &yes, sizeof yes);
+
+ // Linux-ism: Set socket options to optimize for thin streams
+ // See http://lwn.net/Articles/308919/ and
+ // Documentation/networking/tcp-thin.txt .. Kernel 3.2+
+#ifdef TCP_THIN_LINEAR_TIMEOUTS
+ fd.setsockopt(IPPROTO_TCP, TCP_THIN_LINEAR_TIMEOUTS, &yes, sizeof yes);
+#endif
+#ifdef TCP_THIN_DUPACK
+ fd.setsockopt(IPPROTO_TCP, TCP_THIN_DUPACK, &yes, sizeof yes);
+#endif
+
+ readfds.set(fd);
+
+ fd.fcntl(F_SETFL, O_NONBLOCK);
+
+ set_session(fd, make_unique<Session>());
+ Session *s = get_session(fd);
+ s->fd = fd;
+ s->rdata.new_(RFIFO_SIZE);
+ s->wdata.new_(WFIFO_SIZE);
+
+ s->max_rdata = RFIFO_SIZE;
+ s->max_wdata = WFIFO_SIZE;
+ s->func_recv = recv_to_fifo;
+ s->func_send = send_from_fifo;
+ s->func_parse = default_func_parse;
+ s->client_ip = IP4Address(client_address.sin_addr);
+ s->created = TimeT::now();
+ s->connected = 0;
+}
+
+Session *make_listen_port(uint16_t port)
+{
+ struct sockaddr_in server_address;
+ io::FD fd = io::FD::socket(AF_INET, SOCK_STREAM, 0);
+ if (fd == io::FD())
+ {
+ perror("socket");
+ return nullptr;
+ }
+ if (fd_max <= fd.uncast_dammit())
+ fd_max = fd.uncast_dammit() + 1;
+
+ fd.fcntl(F_SETFL, O_NONBLOCK);
+
+ const int yes = 1;
+ /// Allow to bind() again after the server restarts.
+ // Since the socket is still in the TIME_WAIT, there's a possibility
+ // that formerly lost packets might be delivered and confuse the server.
+ fd.setsockopt(SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes);
+ /// Send packets as soon as possible
+ /// even if the kernel thinks there is too little for it to be worth it!
+ // I'm not convinced this is a good idea; although in minimizes the
+ // latency for an individual write, it increases traffic in general.
+ fd.setsockopt(IPPROTO_TCP, TCP_NODELAY, &yes, sizeof yes);
+
+ server_address.sin_family = AF_INET;
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wold-style-cast"
+#if __GNUC__ > 4 || __GNUC_MINOR__ >= 8
+# pragma GCC diagnostic ignored "-Wuseless-cast"
+#endif
+ server_address.sin_addr.s_addr = htonl(INADDR_ANY);
+ server_address.sin_port = htons(port);
+#pragma GCC diagnostic pop
+
+ if (fd.bind(reinterpret_cast<struct sockaddr *>(&server_address),
+ sizeof(server_address)) == -1)
+ {
+ perror("bind");
+ exit(1);
+ }
+ if (fd.listen(5) == -1)
+ { /* error */
+ perror("listen");
+ exit(1);
+ }
+
+ readfds.set(fd);
+
+ set_session(fd, make_unique<Session>());
+ Session *s = get_session(fd);
+ s->fd = fd;
+
+ s->func_recv = connect_client;
+ s->created = TimeT::now();
+ s->connected = 1;
+
+ return s;
+}
+
+Session *make_connection(IP4Address ip, uint16_t port)
+{
+ struct sockaddr_in server_address;
+ io::FD fd = io::FD::socket(AF_INET, SOCK_STREAM, 0);
+ if (fd == io::FD())
+ {
+ perror("socket");
+ return nullptr;
+ }
+ if (fd_max <= fd.uncast_dammit())
+ fd_max = fd.uncast_dammit() + 1;
+
+ const int yes = 1;
+ /// Allow to bind() again after the server restarts.
+ // Since the socket is still in the TIME_WAIT, there's a possibility
+ // that formerly lost packets might be delivered and confuse the server.
+ fd.setsockopt(SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes);
+ /// Send packets as soon as possible
+ /// even if the kernel thinks there is too little for it to be worth it!
+ // I'm not convinced this is a good idea; although in minimizes the
+ // latency for an individual write, it increases traffic in general.
+ fd.setsockopt(IPPROTO_TCP, TCP_NODELAY, &yes, sizeof yes);
+
+ server_address.sin_family = AF_INET;
+ server_address.sin_addr = in_addr(ip);
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wold-style-cast"
+#if __GNUC__ > 4 || __GNUC_MINOR__ >= 8
+# pragma GCC diagnostic ignored "-Wuseless-cast"
+#endif
+ server_address.sin_port = htons(port);
+#pragma GCC diagnostic pop
+
+ fd.fcntl(F_SETFL, O_NONBLOCK);
+
+ /// Errors not caught - we must not block
+ /// Let the main select() loop detect when we know the state
+ fd.connect(reinterpret_cast<struct sockaddr *>(&server_address),
+ sizeof(struct sockaddr_in));
+
+ readfds.set(fd);
+
+ set_session(fd, make_unique<Session>());
+ Session *s = get_session(fd);
+ s->fd = fd;
+ s->rdata.new_(RFIFO_SIZE);
+ s->wdata.new_(WFIFO_SIZE);
+
+ s->max_rdata = RFIFO_SIZE;
+ s->max_wdata = WFIFO_SIZE;
+ s->func_recv = recv_to_fifo;
+ s->func_send = send_from_fifo;
+ s->func_parse = default_func_parse;
+ s->created = TimeT::now();
+ s->connected = 1;
+
+ return s;
+}
+
+void delete_session(Session *s)
+{
+ if (!s)
+ return;
+
+ io::FD fd = s->fd;
+ // If this was the highest fd, decrease it
+ // We could add a loop to decrement fd_max further for every null session,
+ // but this is cheap and good enough for the typical case
+ if (fd.uncast_dammit() == fd_max - 1)
+ fd_max--;
+ readfds.clr(fd);
+ {
+ s->rdata.delete_();
+ s->wdata.delete_();
+ s->session_data.reset();
+ reset_session(fd);
+ }
+
+ // just close() would try to keep sending buffers
+ fd.shutdown(SHUT_RDWR);
+ fd.close();
+}
+
+void realloc_fifo(Session *s, size_t rfifo_size, size_t wfifo_size)
+{
+ if (s->max_rdata != rfifo_size && s->rdata_size < rfifo_size)
+ {
+ s->rdata.resize(rfifo_size);
+ s->max_rdata = rfifo_size;
+ }
+ if (s->max_wdata != wfifo_size && s->wdata_size < wfifo_size)
+ {
+ s->wdata.resize(wfifo_size);
+ s->max_wdata = wfifo_size;
+ }
+}
+
+void WFIFOSET(Session *s, size_t len)
+{
+ if (s->wdata_size + len + 16384 > s->max_wdata)
+ {
+ realloc_fifo(s, s->max_rdata, s->max_wdata << 1);
+ PRINTF("socket: %d wdata expanded to %zu bytes.\n", s, s->max_wdata);
+ }
+ if (s->wdata_size + len + 2048 < s->max_wdata)
+ s->wdata_size += len;
+ else
+ FPRINTF(stderr, "socket: %d wdata lost !!\n", s), abort();
+}
+
+void do_sendrecv(interval_t next_ms)
+{
+ bool any = false;
+ io::FD_Set rfd = readfds, wfd;
+ for (io::FD i : iter_fds())
+ {
+ Session *s = get_session(i);
+ if (s)
+ {
+ any = true;
+ if (s->wdata_size)
+ wfd.set(i);
+ }
+ }
+ if (!any)
+ {
+ if (!has_timers())
+ {
+ PRINTF("Shutting down - nothing to do\n");
+ runflag = false;
+ }
+ return;
+ }
+ struct timeval timeout;
+ {
+ std::chrono::seconds next_s = std::chrono::duration_cast<std::chrono::seconds>(next_ms);
+ std::chrono::microseconds next_us = next_ms - next_s;
+ timeout.tv_sec = next_s.count();
+ timeout.tv_usec = next_us.count();
+ }
+ if (io::FD_Set::select(fd_max, &rfd, &wfd, NULL, &timeout) <= 0)
+ return;
+ for (io::FD i : iter_fds())
+ {
+ Session *s = get_session(i);
+ if (!s)
+ continue;
+ if (wfd.isset(i))
+ {
+ if (s->func_send)
+ //send_from_fifo(i);
+ s->func_send(s);
+ }
+ if (rfd.isset(i))
+ {
+ if (s->func_recv)
+ //recv_to_fifo(i);
+ //or connect_client(i);
+ s->func_recv(s);
+ }
+ }
+}
+
+void do_parsepacket(void)
+{
+ for (io::FD i : iter_fds())
+ {
+ Session *s = get_session(i);
+ if (!s)
+ continue;
+ if (!s->connected
+ && static_cast<time_t>(TimeT::now()) - static_cast<time_t>(s->created) > CONNECT_TIMEOUT)
+ {
+ PRINTF("Session #%d timed out\n", s);
+ s->eof = 1;
+ }
+ if (!s->rdata_size && !s->eof)
+ continue;
+ if (s->func_parse)
+ {
+ s->func_parse(s);
+ /// some func_parse may call delete_session
+ s = get_session(i);
+ if (s && s->eof)
+ {
+ delete_session(s);
+ s = nullptr;
+ }
+ if (!s)
+ continue;
+ }
+ /// Reclaim buffer space for what was read
+ RFIFOFLUSH(s);
+ }
+}
+
+void RFIFOSKIP(Session *s, size_t len)
+{
+ s->rdata_pos += len;
+
+ if (s->rdata_size < s->rdata_pos)
+ {
+ FPRINTF(stderr, "too many skip\n");
+ abort();
+ }
+}
diff --git a/src/mmo/socket.hpp b/src/mmo/socket.hpp
new file mode 100644
index 0000000..a77f512
--- /dev/null
+++ b/src/mmo/socket.hpp
@@ -0,0 +1,373 @@
+#ifndef TMWA_MMO_SOCKET_HPP
+#define TMWA_MMO_SOCKET_HPP
+
+# include "../sanity.hpp"
+
+# include <netinet/in.h>
+
+# include <cstdio>
+
+# include <array>
+
+# include "../compat/rawmem.hpp"
+
+# include "../strings/astring.hpp"
+# include "../strings/vstring.hpp"
+# include "../strings/xstring.hpp"
+
+# include "../io/fd.hpp"
+
+# include "dumb_ptr.hpp"
+# include "ip.hpp"
+# include "utils.hpp"
+# include "timer.t.hpp"
+
+struct SessionData
+{
+};
+struct SessionDeleter
+{
+ // defined per-server
+ void operator()(SessionData *sd);
+};
+
+// Struct declaration
+
+struct Session
+{
+ /// Checks whether a newly-connected socket actually does anything
+ TimeT created;
+ bool connected;
+
+ /// Flag needed since structure must be freed in a server-dependent manner
+ bool eof;
+
+ /// Since this is a single-threaded application, it can't block
+ /// These are the read/write queues
+ dumb_ptr<uint8_t[]> rdata, wdata;
+ size_t max_rdata, max_wdata;
+ /// How much is actually in the queue
+ size_t rdata_size, wdata_size;
+ /// How much has already been read from the queue
+ /// Note that there is no need for a wdata_pos
+ size_t rdata_pos;
+
+ IP4Address client_ip;
+
+ /// Send or recieve
+ /// Only called when select() indicates the socket is ready
+ /// If, after that, nothing is read, it sets eof
+ // These could probably be hard-coded with a little work
+ void (*func_recv)(Session *);
+ void (*func_send)(Session *);
+ /// This is the important one
+ /// Set to different functions depending on whether the connection
+ /// is a player or a server/ladmin
+ /// Can be set explicitly or via set_defaultparse
+ void (*func_parse)(Session *);
+ /// Server-specific data type
+ std::unique_ptr<SessionData, SessionDeleter> session_data;
+
+ io::FD fd;
+};
+
+inline
+int convert_for_printf(Session *s)
+{
+ return s->fd.uncast_dammit();
+}
+
+// save file descriptors for important stuff
+constexpr int SOFT_LIMIT = FD_SETSIZE - 50;
+
+// socket timeout to establish a full connection in seconds
+constexpr int CONNECT_TIMEOUT = 15;
+
+
+void set_session(io::FD fd, std::unique_ptr<Session> sess);
+Session *get_session(io::FD fd);
+void reset_session(io::FD fd);
+int get_fd_max();
+
+class IncrFD
+{
+public:
+ static
+ io::FD inced(io::FD v)
+ {
+ return io::FD::cast_dammit(v.uncast_dammit() + 1);
+ }
+};
+IteratorPair<ValueIterator<io::FD, IncrFD>> iter_fds();
+
+
+/// open a socket, bind, and listen. Return an fd, or -1 if socket() fails,
+/// but exit if bind() or listen() fails
+Session *make_listen_port(uint16_t port);
+/// Connect to an address, return a connected socket or -1
+// FIXME - this is IPv4 only!
+Session *make_connection(IP4Address ip, uint16_t port);
+/// free() the structure and close() the fd
+void delete_session(Session *);
+/// Make a the internal queues bigger
+void realloc_fifo(Session *s, size_t rfifo_size, size_t wfifo_size);
+/// Update all sockets that can be read/written from the queues
+void do_sendrecv(interval_t next);
+/// Call the parser function for every socket that has read data
+void do_parsepacket(void);
+
+/// Change the default parser for newly connected clients
+// typically called once per server, but individual clients may identify
+// themselves as servers
+void set_defaultparse(void(*defaultparse)(Session *));
+
+template<class T>
+uint8_t *pod_addressof_m(T& structure)
+{
+ static_assert(is_trivially_copyable<T>::value, "Can only byte-copy POD-ish structs");
+ return &reinterpret_cast<uint8_t&>(structure);
+}
+
+template<class T>
+const uint8_t *pod_addressof_c(const T& structure)
+{
+ static_assert(is_trivially_copyable<T>::value, "Can only byte-copy POD-ish structs");
+ return &reinterpret_cast<const uint8_t&>(structure);
+}
+
+
+/// Check how much can be read
+inline
+size_t RFIFOREST(Session *s)
+{
+ return s->rdata_size - s->rdata_pos;
+}
+/// Read from the queue
+inline
+const void *RFIFOP(Session *s, size_t pos)
+{
+ return &s->rdata[s->rdata_pos + pos];
+}
+inline
+uint8_t RFIFOB(Session *s, size_t pos)
+{
+ return *static_cast<const uint8_t *>(RFIFOP(s, pos));
+}
+inline
+uint16_t RFIFOW(Session *s, size_t pos)
+{
+ return *static_cast<const uint16_t *>(RFIFOP(s, pos));
+}
+inline
+uint32_t RFIFOL(Session *s, size_t pos)
+{
+ return *static_cast<const uint32_t *>(RFIFOP(s, pos));
+}
+template<class T>
+void RFIFO_STRUCT(Session *s, size_t pos, T& structure)
+{
+ really_memcpy(pod_addressof_m(structure), static_cast<const uint8_t *>(RFIFOP(s, pos)), sizeof(T));
+}
+inline
+IP4Address RFIFOIP(Session *s, size_t pos)
+{
+ IP4Address o;
+ RFIFO_STRUCT(s, pos, o);
+ return o;
+}
+template<uint8_t len>
+inline
+VString<len-1> RFIFO_STRING(Session *s, size_t pos)
+{
+ const char *const begin = static_cast<const char *>(RFIFOP(s, pos));
+ const char *const end = begin + len-1;
+ const char *const mid = std::find(begin, end, '\0');
+ return XString(begin, mid, nullptr);
+}
+inline
+AString RFIFO_STRING(Session *s, size_t pos, size_t len)
+{
+ const char *const begin = static_cast<const char *>(RFIFOP(s, pos));
+ const char *const end = begin + len;
+ const char *const mid = std::find(begin, end, '\0');
+ return XString(begin, mid, nullptr);
+}
+inline
+void RFIFO_BUF_CLONE(Session *s, uint8_t *buf, size_t len)
+{
+ really_memcpy(buf, static_cast<const uint8_t *>(RFIFOP(s, 0)), len);
+}
+
+/// Done reading
+void RFIFOSKIP(Session *s, size_t len);
+
+/// Read from an arbitrary buffer
+inline
+const void *RBUFP(const uint8_t *p, size_t pos)
+{
+ return p + pos;
+}
+inline
+uint8_t RBUFB(const uint8_t *p, size_t pos)
+{
+ return *static_cast<const uint8_t *>(RBUFP(p, pos));
+}
+inline
+uint16_t RBUFW(const uint8_t *p, size_t pos)
+{
+ return *static_cast<const uint16_t *>(RBUFP(p, pos));
+}
+inline
+uint32_t RBUFL(const uint8_t *p, size_t pos)
+{
+ return *static_cast<const uint32_t *>(RBUFP(p, pos));
+}
+template<class T>
+void RBUF_STRUCT(const uint8_t *p, size_t pos, T& structure)
+{
+ really_memcpy(pod_addressof_m(structure), p + pos, sizeof(T));
+}
+inline
+IP4Address RBUFIP(const uint8_t *p, size_t pos)
+{
+ IP4Address o;
+ RBUF_STRUCT(p, pos, o);
+ return o;
+}
+template<uint8_t len>
+inline
+VString<len-1> RBUF_STRING(const uint8_t *p, size_t pos)
+{
+ const char *const begin = static_cast<const char *>(RBUFP(p, pos));
+ const char *const end = begin + len-1;
+ const char *const mid = std::find(begin, end, '\0');
+ return XString(begin, mid, nullptr);
+}
+inline
+AString RBUF_STRING(const uint8_t *p, size_t pos, size_t len)
+{
+ const char *const begin = static_cast<const char *>(RBUFP(p, pos));
+ const char *const end = begin + len;
+ const char *const mid = std::find(begin, end, '\0');
+ return XString(begin, mid, nullptr);
+}
+
+
+/// Unused - check how much data can be written
+// the existence of this seems scary
+inline
+size_t WFIFOSPACE(Session *s)
+{
+ return s->max_wdata - s->wdata_size;
+}
+/// Write to the queue
+inline
+void *WFIFOP(Session *s, size_t pos)
+{
+ return &s->wdata[s->wdata_size + pos];
+}
+inline
+uint8_t& WFIFOB(Session *s, size_t pos)
+{
+ return *static_cast<uint8_t *>(WFIFOP(s, pos));
+}
+inline
+uint16_t& WFIFOW(Session *s, size_t pos)
+{
+ return *static_cast<uint16_t *>(WFIFOP(s, pos));
+}
+inline
+uint32_t& WFIFOL(Session *s, size_t pos)
+{
+ return *static_cast<uint32_t *>(WFIFOP(s, pos));
+}
+template<class T>
+void WFIFO_STRUCT(Session *s, size_t pos, T& structure)
+{
+ really_memcpy(static_cast<uint8_t *>(WFIFOP(s, pos)), pod_addressof_c(structure), sizeof(T));
+}
+inline
+IP4Address& WFIFOIP(Session *s, size_t pos)
+{
+ static_assert(is_trivially_copyable<IP4Address>::value, "That was the whole point");
+ return *static_cast<IP4Address *>(WFIFOP(s, pos));
+}
+inline
+void WFIFO_STRING(Session *s, size_t pos, XString str, size_t len)
+{
+ char *const begin = static_cast<char *>(WFIFOP(s, pos));
+ char *const end = begin + len;
+ char *const mid = std::copy(str.begin(), str.end(), begin);
+ std::fill(mid, end, '\0');
+}
+inline
+void WFIFO_ZERO(Session *s, size_t pos, size_t len)
+{
+ uint8_t *b = static_cast<uint8_t *>(WFIFOP(s, pos));
+ uint8_t *e = b + len;
+ std::fill(b, e, '\0');
+}
+inline
+void WFIFO_BUF_CLONE(Session *s, const uint8_t *buf, size_t len)
+{
+ really_memcpy(static_cast<uint8_t *>(WFIFOP(s, 0)), buf, len);
+}
+
+/// Finish writing
+void WFIFOSET(Session *s, size_t len);
+
+/// Write to an arbitrary buffer
+inline
+void *WBUFP(uint8_t *p, size_t pos)
+{
+ return p + pos;
+}
+inline
+uint8_t& WBUFB(uint8_t *p, size_t pos)
+{
+ return *static_cast<uint8_t *>(WBUFP(p, pos));
+}
+inline
+uint16_t& WBUFW(uint8_t *p, size_t pos)
+{
+ return *static_cast<uint16_t *>(WBUFP(p, pos));
+}
+inline
+uint32_t& WBUFL(uint8_t *p, size_t pos)
+{
+ return *static_cast<uint32_t *>(WBUFP(p, pos));
+}
+template<class T>
+void WBUF_STRUCT(uint8_t *p, size_t pos, T& structure)
+{
+ really_memcpy(p + pos, pod_addressof_c(structure), sizeof(T));
+}
+inline
+IP4Address& WBUFIP(uint8_t *p, size_t pos)
+{
+ return *static_cast<IP4Address *>(WBUFP(p, pos));
+}
+inline
+void WBUF_STRING(uint8_t *p, size_t pos, XString s, size_t len)
+{
+ char *const begin = static_cast<char *>(WBUFP(p, pos));
+ char *const end = begin + len;
+ char *const mid = std::copy(s.begin(), s.end(), begin);
+ std::fill(mid, end, '\0');
+}
+inline
+void WBUF_ZERO(uint8_t *p, size_t pos, size_t len)
+{
+ uint8_t *b = static_cast<uint8_t *>(WBUFP(p, pos));
+ uint8_t *e = b + len;
+ std::fill(b, e, '\0');
+}
+
+inline
+void RFIFO_WFIFO_CLONE(Session *rs, Session *ws, size_t len)
+{
+ really_memcpy(static_cast<uint8_t *>(WFIFOP(ws, 0)),
+ static_cast<const uint8_t *>(RFIFOP(rs, 0)), len);
+}
+
+#endif // TMWA_MMO_SOCKET_HPP
diff --git a/src/mmo/timer.cpp b/src/mmo/timer.cpp
new file mode 100644
index 0000000..b5a2a5e
--- /dev/null
+++ b/src/mmo/timer.cpp
@@ -0,0 +1,201 @@
+#include "timer.hpp"
+
+#include <sys/stat.h>
+#include <sys/time.h>
+
+#include <cassert>
+#include <cstring>
+
+#include <queue>
+
+#include "../strings/zstring.hpp"
+
+#include "../io/cxxstdio.hpp"
+
+#include "utils.hpp"
+
+#include "../poison.hpp"
+
+struct TimerData
+{
+ /// This will be reset on call, to avoid problems.
+ Timer *owner;
+
+ /// When it will be triggered
+ tick_t tick;
+ /// What will be done
+ timer_func func;
+ /// Repeat rate - 0 for oneshot
+ interval_t interval;
+
+ TimerData(Timer *o, tick_t t, timer_func f, interval_t i)
+ : owner(o)
+ , tick(t)
+ , func(std::move(f))
+ , interval(i)
+ {}
+};
+
+struct TimerCompare
+{
+ /// implement "less than"
+ bool operator() (dumb_ptr<TimerData> l, dumb_ptr<TimerData> r)
+ {
+ // C++ provides a max-heap, but we want
+ // the smallest tick to be the head (a min-heap).
+ return l->tick > r->tick;
+ }
+};
+
+static
+std::priority_queue<dumb_ptr<TimerData>, std::vector<dumb_ptr<TimerData>>, TimerCompare> timer_heap;
+
+
+tick_t gettick_cache;
+
+tick_t milli_clock::now(void) noexcept
+{
+ struct timeval tval;
+ // BUG: This will cause strange behavior if the system clock is changed!
+ // it should be reimplemented in terms of clock_gettime(CLOCK_MONOTONIC, )
+ gettimeofday(&tval, NULL);
+ return gettick_cache = tick_t(std::chrono::seconds(tval.tv_sec)
+ + std::chrono::duration_cast<std::chrono::milliseconds>(
+ std::chrono::microseconds(tval.tv_usec)));
+}
+
+static
+void do_nothing(TimerData *, tick_t)
+{
+}
+
+void Timer::cancel()
+{
+ if (!td)
+ return;
+
+ assert (this == td->owner);
+ td->owner = nullptr;
+ td->func = do_nothing;
+ td->interval = interval_t::zero();
+ td = nullptr;
+}
+
+void Timer::detach()
+{
+ assert (this == td->owner);
+ td->owner = nullptr;
+ td = nullptr;
+}
+
+static
+void push_timer_heap(dumb_ptr<TimerData> td)
+{
+ timer_heap.push(td);
+}
+
+static
+dumb_ptr<TimerData> top_timer_heap(void)
+{
+ if (timer_heap.empty())
+ return dumb_ptr<TimerData>();
+ return timer_heap.top();
+}
+
+static
+void pop_timer_heap(void)
+{
+ timer_heap.pop();
+}
+
+Timer::Timer(tick_t tick, timer_func func, interval_t interval)
+: td(dumb_ptr<TimerData>::make(this, tick, std::move(func), interval))
+{
+ assert (interval >= interval_t::zero());
+
+ push_timer_heap(td);
+}
+
+Timer::Timer(Timer&& t)
+: td(t.td)
+{
+ t.td = nullptr;
+ if (td)
+ {
+ assert (td->owner == &t);
+ td->owner = this;
+ }
+}
+
+Timer& Timer::operator = (Timer&& t)
+{
+ std::swap(td, t.td);
+ if (td)
+ {
+ assert (td->owner == &t);
+ td->owner = this;
+ }
+ if (t.td)
+ {
+ assert (t.td->owner == this);
+ t.td->owner = &t;
+ }
+ return *this;
+}
+
+interval_t do_timer(tick_t tick)
+{
+ /// Number of milliseconds until it calls this again
+ // this says to wait 1 sec if all timers get popped
+ interval_t nextmin = std::chrono::seconds(1);
+
+ while (dumb_ptr<TimerData> td = top_timer_heap())
+ {
+ // while the heap is not empty and
+ if (td->tick > tick)
+ {
+ /// Return the time until the next timer needs to goes off
+ nextmin = td->tick - tick;
+ break;
+ }
+ pop_timer_heap();
+
+ // Prevent destroying the object we're in.
+ // Note: this would be surprising in an interval timer,
+ // but all interval timers do an immediate explicit detach().
+ if (td->owner)
+ td->owner->detach();
+ // If we are too far past the requested tick, call with
+ // the current tick instead to fix reregistration problems
+ if (td->tick + std::chrono::seconds(1) < tick)
+ td->func(td.operator->(), tick);
+ else
+ td->func(td.operator->(), td->tick);
+
+ if (td->interval == interval_t::zero())
+ {
+ td.delete_();
+ continue;
+ }
+ if (td->tick + std::chrono::seconds(1) < tick)
+ td->tick = tick + td->interval;
+ else
+ td->tick += td->interval;
+ push_timer_heap(td);
+ }
+
+ return std::max(nextmin, std::chrono::milliseconds(10));
+}
+
+tick_t file_modified(ZString name)
+{
+ struct stat buf;
+ if (stat(name.c_str(), &buf))
+ return tick_t();
+ return tick_t(std::chrono::seconds(buf.st_mtime));
+}
+
+bool has_timers()
+{
+ return !timer_heap.empty();
+}
diff --git a/src/mmo/timer.hpp b/src/mmo/timer.hpp
new file mode 100644
index 0000000..01b8623
--- /dev/null
+++ b/src/mmo/timer.hpp
@@ -0,0 +1,30 @@
+#ifndef TMWA_MMO_TIMER_HPP
+#define TMWA_MMO_TIMER_HPP
+
+# include "timer.t.hpp"
+
+# include "../sanity.hpp"
+
+# include "../strings/fwd.hpp"
+
+// updated automatically when using milli_clock::now()
+// which is done only by core.cpp
+extern tick_t gettick_cache;
+
+inline
+tick_t gettick(void)
+{
+ return gettick_cache;
+}
+
+/// Do all timers scheduled before tick, and return the number of
+/// milliseconds until the next timer happens
+interval_t do_timer(tick_t tick);
+
+/// Stat a file, and return its modification time, truncated to seconds.
+tick_t file_modified(ZString name);
+
+/// Check if there are any events at all scheduled.
+bool has_timers();
+
+#endif // TMWA_MMO_TIMER_HPP
diff --git a/src/mmo/timer.t.hpp b/src/mmo/timer.t.hpp
new file mode 100644
index 0000000..dcc88f8
--- /dev/null
+++ b/src/mmo/timer.t.hpp
@@ -0,0 +1,68 @@
+#ifndef TMWA_MMO_TIMER_T_HPP
+#define TMWA_MMO_TIMER_T_HPP
+
+# include <chrono>
+# include <functional>
+
+# include "dumb_ptr.hpp"
+
+struct TimerData;
+
+/// An implementation of the C++ "clock" concept, exposing
+/// durations in milliseconds.
+class milli_clock
+{
+public:
+ typedef std::chrono::milliseconds duration;
+ typedef duration::rep rep;
+ typedef duration::period period;
+ typedef std::chrono::time_point<milli_clock, duration> time_point;
+ static const bool is_steady = true; // assumed - not necessarily true
+
+ static time_point now() noexcept;
+};
+
+/// A point in time.
+typedef milli_clock::time_point tick_t;
+/// The difference between two points in time.
+typedef milli_clock::duration interval_t;
+/// (to get additional arguments, use std::bind or a lambda).
+typedef std::function<void (TimerData *, tick_t)> timer_func;
+
+class Timer
+{
+ friend struct TimerData;
+ dumb_ptr<TimerData> td;
+
+ Timer(const Timer&) = delete;
+ Timer& operator = (const Timer&) = delete;
+public:
+ /// Don't own anything yet.
+ Timer() = default;
+ /// Schedule a timer for the given tick.
+ /// If you do not wish to keep track of it, call disconnect().
+ /// Otherwise, you may cancel() or replace (operator =) it later.
+ ///
+ /// If the interval argument is given, the timer will reschedule
+ /// itself again forever. Otherwise, it will disconnect() itself
+ /// just BEFORE it is called.
+ Timer(tick_t tick, timer_func func, interval_t interval=interval_t::zero());
+
+ Timer(Timer&& t);
+ Timer& operator = (Timer&& t);
+ ~Timer() { cancel(); }
+
+ /// Cancel the delivery of this timer's function, and make it falsy.
+ /// Implementation note: this doesn't actually remove it, just sets
+ /// the functor to do_nothing, and waits for the tick before removing.
+ void cancel();
+ /// Make it falsy without cancelling the timer,
+ void detach();
+
+ /// Check if there is a timer connected.
+ explicit operator bool() { return bool(td); }
+ /// Check if there is no connected timer.
+ bool operator !() { return !td; }
+};
+
+#endif // TMWA_MMO_TIMER_T_HPP
diff --git a/src/mmo/utils.cpp b/src/mmo/utils.cpp
new file mode 100644
index 0000000..0dbf145
--- /dev/null
+++ b/src/mmo/utils.cpp
@@ -0,0 +1,101 @@
+#include "utils.hpp"
+
+#include <netinet/in.h>
+#include <sys/time.h>
+
+#include <algorithm>
+
+#include "../strings/astring.hpp"
+#include "../strings/zstring.hpp"
+#include "../strings/xstring.hpp"
+
+#include "../io/cxxstdio.hpp"
+#include "../io/write.hpp"
+
+#include "extract.hpp"
+
+#include "../poison.hpp"
+
+//---------------------------------------------------
+// E-mail check: return 0 (not correct) or 1 (valid).
+//---------------------------------------------------
+bool e_mail_check(XString email)
+{
+ // athena limits
+ if (email.size() < 3 || email.size() > 39)
+ return 0;
+
+ // part of RFC limits (official reference of e-mail description)
+ XString::iterator at = std::find(email.begin(), email.end(), '@');
+ if (at == email.end())
+ return 0;
+ XString username = email.xislice_h(at);
+ XString hostname = email.xislice_t(at + 1);
+ if (!username || !hostname)
+ return 0;
+ if (hostname.contains('@'))
+ return 0;
+ if (hostname.front() == '.' || hostname.back() == '.')
+ return 0;
+ if (hostname.contains_seq(".."))
+ return 0;
+ if (email.contains_any(" ;"))
+ return 0;
+ return email.is_print();
+}
+
+//-------------------------------------------------
+// Return numerical value of a switch configuration
+// on/off, english, français, deutsch, español
+//-------------------------------------------------
+int config_switch(ZString str)
+{
+ if (str == "true" || str == "on" || str == "yes"
+ || str == "oui" || str == "ja"
+ || str == "si")
+ return 1;
+ if (str == "false" || str == "off" || str == "no"
+ || str == "non" || str == "nein")
+ return 0;
+
+ int rv;
+ if (extract(str, &rv))
+ return rv;
+ FPRINTF(stderr, "Fatal: bad option value %s", str);
+ abort();
+}
+
+static_assert(sizeof(timestamp_seconds_buffer) == 20, "seconds buffer");
+static_assert(sizeof(timestamp_milliseconds_buffer) == 24, "millis buffer");
+
+void stamp_time(timestamp_seconds_buffer& out, const TimeT *t)
+{
+ struct tm when = t ? *t : TimeT::now();
+ char buf[20];
+ strftime(buf, 20, "%Y-%m-%d %H:%M:%S", &when);
+ out = stringish<timestamp_seconds_buffer>(const_(buf));
+}
+void stamp_time(timestamp_milliseconds_buffer& out)
+{
+ struct timeval tv;
+ gettimeofday(&tv, NULL);
+ struct tm when = TimeT(tv.tv_sec);
+ char buf[24];
+ strftime(buf, 20, "%Y-%m-%d %H:%M:%S", &when);
+ sprintf(buf + 19, ".%03d", static_cast<int>(tv.tv_usec / 1000));
+ out = stringish<timestamp_milliseconds_buffer>(const_(buf));
+}
+
+void log_with_timestamp(io::WriteFile& out, XString line)
+{
+ if (!line)
+ {
+ out.put('\n');
+ return;
+ }
+ timestamp_milliseconds_buffer tmpstr;
+ stamp_time(tmpstr);
+ out.really_put(tmpstr.data(), tmpstr.size());
+ out.really_put(": ", 2);
+ out.put_line(line);
+}
diff --git a/src/mmo/utils.hpp b/src/mmo/utils.hpp
new file mode 100644
index 0000000..5e9de26
--- /dev/null
+++ b/src/mmo/utils.hpp
@@ -0,0 +1,116 @@
+#ifndef TMWA_MMO_UTILS_HPP
+#define TMWA_MMO_UTILS_HPP
+
+# include "../sanity.hpp"
+
+# include <cstdio>
+
+# include <type_traits>
+
+# include "../strings/fwd.hpp"
+# include "../strings/vstring.hpp"
+
+# include "../generic/const_array.hpp"
+# include "../generic/operators.hpp"
+
+# include "../io/fwd.hpp"
+
+template<class T>
+struct is_trivially_copyable
+: std::integral_constant<bool,
+ // come back when GCC actually implements the public traits properly
+ __has_trivial_copy(T)
+ && __has_trivial_assign(T)
+ && __has_trivial_destructor(T)>
+{};
+
+bool e_mail_check(XString email);
+int config_switch (ZString str);
+
+template<class T>
+void really_memzero_this(T *v)
+{
+ static_assert(is_trivially_copyable<T>::value, "only for mostly-pod types");
+ static_assert(std::is_class<T>::value || std::is_union<T>::value, "Only for user-defined structures (for now)");
+ memset(v, '\0', sizeof(*v));
+}
+template<class T, size_t n>
+void really_memzero_this(T (&)[n]) = delete;
+
+// Exists in place of time_t, to give it a predictable printf-format.
+// (on x86 and amd64, time_t == long, but not on x32)
+static_assert(sizeof(long long) >= sizeof(time_t), "long long >= time_t");
+struct TimeT : Comparable
+{
+ long long value;
+
+ // conversion
+ TimeT(time_t t=0) : value(t) {}
+ TimeT(struct tm t) : value(timegm(&t)) {}
+ operator time_t() const { return value; }
+ operator struct tm() const { time_t v = value; return *gmtime(&v); }
+
+ explicit operator bool() const { return value; }
+ bool operator !() const { return !value; }
+
+ // prevent surprises
+ template<class T>
+ TimeT(T) = delete;
+ template<class T>
+ operator T() const = delete;
+
+ static
+ TimeT now()
+ {
+ // poisoned, but this is still in header-land
+ return time(NULL);
+ }
+
+ bool error() const
+ {
+ return value == -1;
+ }
+ bool okay() const
+ {
+ return !error();
+ }
+};
+
+inline
+long long convert_for_printf(TimeT t)
+{
+ return t.value;
+}
+
+inline
+long long& convert_for_scanf(TimeT& t)
+{
+ return t.value;
+}
+
+struct timestamp_seconds_buffer : VString<19> {};
+struct timestamp_milliseconds_buffer : VString<23> {};
+void stamp_time(timestamp_seconds_buffer&, const TimeT *t=nullptr);
+void stamp_time(timestamp_milliseconds_buffer&);
+
+void log_with_timestamp(io::WriteFile& out, XString line);
+
+// TODO VString?
+# define TIMESTAMP_DUMMY "YYYY-MM-DD HH:MM:SS"
+static_assert(sizeof(TIMESTAMP_DUMMY) == sizeof(timestamp_seconds_buffer),
+ "timestamp size");
+# define WITH_TIMESTAMP(str) str TIMESTAMP_DUMMY
+// str: prefix: YYYY-MM-DD HH:MM:SS
+// sizeof: 01234567890123456789012345678
+// str + sizeof: ^
+// -1: ^
+// there's probably a better way to do this now
+# define REPLACE_TIMESTAMP(str, t) \
+ stamp_time( \
+ reinterpret_cast<timestamp_seconds_buffer *>( \
+ str + sizeof(str) \
+ )[-1], \
+ &t \
+ )
+
+#endif // TMWA_MMO_UTILS_HPP
diff --git a/src/mmo/version.cpp b/src/mmo/version.cpp
new file mode 100644
index 0000000..dd18fe1
--- /dev/null
+++ b/src/mmo/version.cpp
@@ -0,0 +1,55 @@
+#include "version.hpp"
+
+#include "../conf/version.hpp"
+
+#include "../strings/xstring.hpp"
+
+#include "extract.hpp"
+
+#include "../poison.hpp"
+
+Version CURRENT_VERSION =
+{
+ VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH,
+ VERSION_DEVEL,
+
+ 0, 0,
+ VENDOR_VERSION,
+};
+Version CURRENT_LOGIN_SERVER_VERSION =
+{
+ VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH,
+ VERSION_DEVEL,
+
+ 0, TMWA_SERVER_LOGIN,
+ VENDOR_VERSION,
+};
+Version CURRENT_CHAR_SERVER_VERSION =
+{
+ VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH,
+ VERSION_DEVEL,
+
+ 0, TMWA_SERVER_CHAR | TMWA_SERVER_INTER,
+ VENDOR_VERSION,
+};
+Version CURRENT_MAP_SERVER_VERSION =
+{
+ VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH,
+ VERSION_DEVEL,
+
+ 0, TMWA_SERVER_MAP,
+ VENDOR_VERSION,
+};
+
+#define S2(a) #a
+#define S(a) S2(a)
+
+const char CURRENT_VERSION_STRING[] = "TMWA "
+ S(VERSION_MAJOR) "." S(VERSION_MINOR) "." S(VERSION_PATCH)
+ " dev" S(VERSION_DEVEL) " (" VENDOR " " S(VENDOR_VERSION) ")";
+
+bool extract(XString str, Version *vers)
+{
+ *vers = {};
+ return extract(str, record<'.'>(&vers->major, &vers->minor, &vers->patch));
+}
diff --git a/src/mmo/version.hpp b/src/mmo/version.hpp
new file mode 100644
index 0000000..420cbf9
--- /dev/null
+++ b/src/mmo/version.hpp
@@ -0,0 +1,69 @@
+#ifndef TMWA_MMO_VERSION_HPP
+#define TMWA_MMO_VERSION_HPP
+
+# include <cstdint>
+
+# include "../strings/fwd.hpp"
+
+// TODO make these bitwise enums
+# define TMWA_FLAG_REGISTRATION 0x01
+
+# define TMWA_SERVER_LOGIN 0x01
+# define TMWA_SERVER_CHAR 0x02
+# define TMWA_SERVER_INTER 0x04
+# define TMWA_SERVER_MAP 0x08
+
+struct Version
+{
+ uint8_t major;
+ uint8_t minor; // flavor1
+ uint8_t patch; // flavor2
+ uint8_t devel; // flavor3
+
+ uint8_t flags;
+ uint8_t which;
+ uint16_t vend;
+ // can't add vendor name yet
+};
+static_assert(sizeof(Version) == 8, "this is sent over the network, can't change");
+
+extern Version CURRENT_VERSION;
+
+extern Version CURRENT_LOGIN_SERVER_VERSION;
+extern Version CURRENT_CHAR_SERVER_VERSION;
+extern Version CURRENT_MAP_SERVER_VERSION;
+
+extern const char CURRENT_VERSION_STRING[];
+
+bool extract(XString str, Version *vers);
+
+constexpr
+bool operator < (Version l, Version r)
+{
+ return (l.major < r.major
+ || (l.major == r.major
+ && (l.minor < r.minor
+ || (l.minor == r.minor
+ && (l.patch < r.patch
+ || (l.patch == r.patch
+ && (l.devel < r.devel
+ || (l.devel == r.devel
+ && l.vend < r.vend))))))));
+}
+constexpr
+bool operator > (Version l, Version r)
+{
+ return r < l;
+}
+constexpr
+bool operator <= (Version l, Version r)
+{
+ return !(r < l);
+}
+constexpr
+bool operator >= (Version l, Version r)
+{
+ return !(l < r);
+}
+
+#endif // TMWA_MMO_VERSION_HPP