From 6abf054b42c0ad41e79b42e5143cfbcb456725f7 Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Sun, 27 May 2018 20:31:36 +0200
Subject: [PATCH 01/62] replace bsdstring functions in cycle_route.

---
 src/move.c         | 55 ++++++++++++++++++++++------------------------
 src/move.test.c    |  1 +
 src/util/strings.c |  5 +++++
 src/util/strings.h |  1 +
 4 files changed, 33 insertions(+), 29 deletions(-)

diff --git a/src/move.c b/src/move.c
index c1ded5faa..a7ae6b018 100644
--- a/src/move.c
+++ b/src/move.c
@@ -1047,15 +1047,17 @@ int movewhere(const unit * u, const char *token, region * r, region ** resultp)
 order * cycle_route(order * ord, const struct locale *lang, int gereist)
 {
     int cm = 0;
-    char tail[1024], *bufp = tail;
-    char neworder[2048], *obuf = neworder;
+    char tail[1024];
+    char neworder[2048];
     char token[128];
     direction_t d = NODIRECTION;
     bool paused = false;
-    bool pause;
     order *norder;
-    size_t size = sizeof(tail) - 1;
+    sbstring sbtail;
+    sbstring sborder;
 
+    sbs_init(&sbtail, tail, sizeof(tail));
+    sbs_init(&sborder, neworder, sizeof(neworder));
     assert(getkeyword(ord) == K_ROUTE);
     tail[0] = '\0';
     neworder[0] = '\0';
@@ -1063,14 +1065,10 @@ order * cycle_route(order * ord, const struct locale *lang, int gereist)
 
     for (cm = 0;; ++cm) {
         const char *s;
-        pause = false;
         s = gettoken(token, sizeof(token));
         if (s && *s) {
             d = get_direction(s, lang);
-            if (d == D_PAUSE) {
-                pause = true;
-            }
-            else if (d == NODIRECTION) {
+            if (d == NODIRECTION) {
                 break;
             }
         }
@@ -1079,38 +1077,37 @@ order * cycle_route(order * ord, const struct locale *lang, int gereist)
         }
         if (cm < gereist) {
             /* TODO: hier sollte keine PAUSE auftreten */
-            assert(!pause);
-            if (!pause) {
+            assert (d != D_PAUSE);
+            if (d != D_PAUSE) {
                 const char *loc = LOC(lang, shortdirections[d]);
                 assert(loc);
-                if (bufp != tail) {
-                    bufp = STRLCPY_EX(bufp, " ", &size, "cycle_route");
+                if (sbs_length(&sbtail) > 0) {
+                    sbs_strcat(&sbtail, " ");
                 }
-                bufp = STRLCPY_EX(bufp, loc, &size, "cycle_route");
+                sbs_strcat(&sbtail, loc);
             }
         }
         else if (strlen(neworder) > sizeof(neworder) / 2)
             break;
-        else if (cm == gereist && !paused && pause) {
+        else if (cm == gereist && !paused && (d == D_PAUSE)) {
             const char *loc = LOC(lang, parameters[P_PAUSE]);
-            bufp = STRLCPY_EX(bufp, " ", &size, "cycle_route");
-            bufp = STRLCPY_EX(bufp, loc, &size, "cycle_route");
+            sbs_strcat(&sbtail, " ");
+            sbs_strcat(&sbtail, loc);
             paused = true;
         }
-        else if (pause) {
-            /* da PAUSE nicht in ein shortdirections[d] umgesetzt wird (ist
-             * hier keine normale direction), muss jede PAUSE einzeln
-             * herausgefiltert und explizit gesetzt werden */
-            if (neworder != obuf) {
-                obuf += str_strlcat(obuf, " ", sizeof(neworder) - (obuf - neworder));
-            }
-            obuf += str_strlcat(obuf, LOC(lang, parameters[P_PAUSE]), sizeof(neworder) - (obuf - neworder));
-        }
         else {
-            if (neworder != obuf) {
-                obuf += str_strlcat(obuf, " ", sizeof(neworder) - (obuf - neworder));
+            if (sbs_length(&sbtail) > 0) {
+                sbs_strcat(&sborder, " ");
+            }
+            if (d == D_PAUSE) {
+                /* da PAUSE nicht in ein shortdirections[d] umgesetzt wird (ist
+                 * hier keine normale direction), muss jede PAUSE einzeln
+                 * herausgefiltert und explizit gesetzt werden */
+                sbs_strcat(&sborder, LOC(lang, parameters[P_PAUSE]));
+            }
+            else {
+                sbs_strcat(&sborder, LOC(lang, shortdirections[d]));
             }
-            obuf += str_strlcat(obuf, LOC(lang, shortdirections[d]), sizeof(neworder) - (obuf - neworder));
         }
     }
 
diff --git a/src/move.test.c b/src/move.test.c
index a2fa4e760..f8c11114f 100644
--- a/src/move.test.c
+++ b/src/move.test.c
@@ -495,6 +495,7 @@ static void test_follow_ship_msg(CuTest * tc) {
 
     follow_ship(u, ord);
 
+    CuAssertPtrEquals(tc, r, u->region);
     CuAssertPtrNotNull(tc, msg = test_find_messagetype(u->faction->msgs, "error18"));
     p = msg->parameters[2].v;
     CuAssertPtrNotNull(tc, p);
diff --git a/src/util/strings.c b/src/util/strings.c
index f4bbd54a5..ef351ab1f 100644
--- a/src/util/strings.c
+++ b/src/util/strings.c
@@ -275,6 +275,11 @@ void sbs_strcpy(struct sbstring *sbs, const char *str)
     sbs->end = sbs->begin + len;
 }
 
+size_t sbs_length(const struct sbstring *sbs)
+{
+    return sbs->end - sbs->begin;
+}
+
 char *str_unescape(char *str) {
     char *read = str, *write = str;
     while (*read) {
diff --git a/src/util/strings.h b/src/util/strings.h
index 2d93b414e..bd6f0de58 100644
--- a/src/util/strings.h
+++ b/src/util/strings.h
@@ -50,6 +50,7 @@ extern "C" {
     void sbs_strcat(struct sbstring *sbs, const char *str);
     void sbs_strncat(struct sbstring *sbs, const char *str, size_t size);
     void sbs_strcpy(struct sbstring *sbs, const char *str);
+    size_t sbs_length(const struct sbstring *sbs);
 
     /* benchmark for units:
      * JENKINS_HASH: 5.25 misses/hit (with good cache behavior)

From cb5d15fee31e1fa4d3971283f674052b5817f43e Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Sun, 3 Jun 2018 05:42:09 +0200
Subject: [PATCH 02/62] eliminate bsdstring.h usage

---
 src/move.c | 19 ++++++++-----------
 1 file changed, 8 insertions(+), 11 deletions(-)

diff --git a/src/move.c b/src/move.c
index a7ae6b018..990d6fc14 100644
--- a/src/move.c
+++ b/src/move.c
@@ -73,7 +73,6 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 #include <util/assert.h>
 #include <util/attrib.h>
 #include <util/base36.h>
-#include <util/bsdstring.h>
 #include <util/gamedata.h>
 #include <util/language.h>
 #include <util/lists.h>
@@ -2247,10 +2246,9 @@ static direction_t hunted_dir(attrib * at, int id)
 int follow_ship(unit * u, order * ord)
 {
     region *rc = u->region;
-    size_t bytes;
+    sbstring sbcmd;
     int  moves, id, speed;
-    char command[256], *bufp = command;
-    size_t size = sizeof(command);
+    char command[256];
     direction_t dir;
 
     if (fval(u, UFL_NOTMOVING)) {
@@ -2286,11 +2284,10 @@ int follow_ship(unit * u, order * ord)
         return 0;
     }
 
-    bufp = command;
-    bytes = slprintf(bufp, size, "%s %s", LOC(u->faction->locale, keyword(K_MOVE)), LOC(u->faction->locale, directions[dir]));
-    assert(bytes <= INT_MAX);
-    if (wrptr(&bufp, &size, (int)bytes) != 0)
-        WARN_STATIC_BUFFER();
+    sbs_init(&sbcmd, command, sizeof(command));
+    sbs_strcpy(&sbcmd, LOC(u->faction->locale, keyword(K_MOVE)));
+    sbs_strcat(&sbcmd, " ");
+    sbs_strcat(&sbcmd, LOC(u->faction->locale, directions[dir]));
 
     moves = 1;
 
@@ -2306,8 +2303,8 @@ int follow_ship(unit * u, order * ord)
     rc = rconnect(rc, dir);
     while (rc && moves < speed && (dir = hunted_dir(rc->attribs, id)) != NODIRECTION) {
         const char *loc = LOC(u->faction->locale, directions[dir]);
-        bufp = STRLCPY_EX(bufp, " ", &size, "hunt");
-        bufp = STRLCPY_EX(bufp, loc, &size, "hunt");
+        sbs_strcat(&sbcmd, " ");
+        sbs_strcat(&sbcmd, loc);
         moves++;
         rc = rconnect(rc, dir);
     }

From 3208164b134b6cab4c2e69d0e087417a022e3b1d Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno@kn-bremen.de>
Date: Sun, 3 Jun 2018 11:01:08 +0200
Subject: [PATCH 03/62] update version

---
 src/kernel/version.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/kernel/version.c b/src/kernel/version.c
index 4e1c49cf4..994291ec2 100644
--- a/src/kernel/version.c
+++ b/src/kernel/version.c
@@ -7,7 +7,7 @@
 
 #ifndef ERESSEA_VERSION
 /* the version number, if it was not passed to make with -D */
-#define ERESSEA_VERSION "3.16.0"
+#define ERESSEA_VERSION "3.17.0"
 #endif
 
 const char *eressea_version(void) {

From 6234ae7e76689da0f6aab0727439e2fc5697a954 Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Sun, 3 Jun 2018 12:27:40 +0200
Subject: [PATCH 04/62] readability: bool vs int

---
 conf/e2/config.json | 2 +-
 conf/e3/config.json | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/conf/e2/config.json b/conf/e2/config.json
index ebc385adf..4da247263 100644
--- a/conf/e2/config.json
+++ b/conf/e2/config.json
@@ -39,7 +39,7 @@
         "entertain.perlevel": 20,
         "taxing.perlevel": 20,
         "nmr.timeout": 5,
-        "nmr.removenewbie": 0,
+        "nmr.removenewbie": false,
         "GiveRestriction": 3,
         "hunger.long": true,
         "init_spells": 0,
diff --git a/conf/e3/config.json b/conf/e3/config.json
index c1fa2725c..b4b6d0e48 100644
--- a/conf/e3/config.json
+++ b/conf/e3/config.json
@@ -58,7 +58,7 @@
         "entertain.base": 0,
         "entertain.perlevel": 20,
         "nmr.timeout": 5,
-        "nmr.removenewbie": 0,
+        "nmr.removenewbie": false,
         "GiveRestriction": 3,
         "healing.forest": 2.0,
         "hunger.long": false,

From 4b5bd11f296485a5b15337815f6fb659eebfae90 Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Sun, 3 Jun 2018 13:29:54 +0200
Subject: [PATCH 05/62] spell_syntax without bsdstring, better tests.

---
 src/report.c      | 139 ++++++++++++++++++++++------------------------
 src/report.h      |   2 +-
 src/report.test.c |  51 +++++------------
 3 files changed, 82 insertions(+), 110 deletions(-)

diff --git a/src/report.c b/src/report.c
index 3976bb05b..55189a66c 100644
--- a/src/report.c
+++ b/src/report.c
@@ -79,7 +79,6 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 /* util includes */
 #include <util/attrib.h>
 #include <util/base36.h>
-#include <util/bsdstring.h>
 #include <util/goodies.h>
 #include <util/language.h>
 #include <util/lists.h>
@@ -243,39 +242,44 @@ static size_t write_spell_modifier(const spell * sp, int flag, const char * str,
     return 0;
 }
 
-void nr_spell_syntax(struct stream *out, spellbook_entry * sbe, const struct locale *lang)
+void nr_spell_syntax(char *buf, size_t size, spellbook_entry * sbe, const struct locale *lang)
 {
-    int bytes;
-    char buf[4096];
-    char *bufp = buf;
-    size_t size = sizeof(buf) - 1;
     const spell *sp = spellref_get(&sbe->spref);
     const char *params = sp->parameter;
+    const char *spname;
+    sbstring sbs;
 
+    sbs_init(&sbs, buf, size);
     if (sp->sptyp & ISCOMBATSPELL) {
-        bytes = (int)str_strlcpy(bufp, LOC(lang, keyword(K_COMBATSPELL)), size);
+        sbs_strcpy(&sbs, LOC(lang, keyword(K_COMBATSPELL)));
     }
     else {
-        bytes = (int)str_strlcpy(bufp, LOC(lang, keyword(K_CAST)), size);
+        sbs_strcpy(&sbs, LOC(lang, keyword(K_CAST)));
     }
-    if (wrptr(&bufp, &size, bytes) != 0)
-        WARN_STATIC_BUFFER();
 
     /* Reihenfolge beachten: Erst REGION, dann STUFE! */
     if (sp->sptyp & FARCASTING) {
-        bytes = snprintf(bufp, size, " [%s x y]", LOC(lang, parameters[P_REGION]));
-        if (wrptr(&bufp, &size, bytes) != 0)
-            WARN_STATIC_BUFFER();
+        sbs_strcat(&sbs, " [");
+        sbs_strcat(&sbs, LOC(lang, parameters[P_REGION]));
+        sbs_strcat(&sbs, " x y]");
     }
     if (sp->sptyp & SPELLLEVEL) {
-        bytes = snprintf(bufp, size, " [%s n]", LOC(lang, parameters[P_LEVEL]));
-        if (wrptr(&bufp, &size, bytes) != 0)
-            WARN_STATIC_BUFFER();
+        sbs_strcat(&sbs, " [");
+        sbs_strcat(&sbs, LOC(lang, parameters[P_LEVEL]));
+        sbs_strcat(&sbs, " n]");
     }
 
-    bytes = (int)snprintf(bufp, size, " \"%s\"", spell_name(sp, lang));
-    if (wrptr(&bufp, &size, bytes) != 0)
-        WARN_STATIC_BUFFER();
+    spname = spell_name(sp, lang);
+    if (strchr(spname, ' ') != NULL) {
+        /* contains spaces, needs quotes */
+        sbs_strcat(&sbs, " '");
+        sbs_strcat(&sbs, spname);
+        sbs_strcat(&sbs, "'");
+    }
+    else {
+        sbs_strcat(&sbs, " ");
+        sbs_strcat(&sbs, spname);
+    }
 
     while (params && *params) {
         typedef struct starget {
@@ -298,52 +302,48 @@ void nr_spell_syntax(struct stream *out, spellbook_entry * sbe, const struct loc
         if (cp == 'u') {
             targetp = targets + 1;
             locp = LOC(lang, targetp->vars);
-            bytes = (int)snprintf(bufp, size, " <%s>", locp);
+            sbs_strcat(&sbs, " <");
+            sbs_strcat(&sbs, locp);
+            sbs_strcat(&sbs, ">");
             if (*params == '+') {
                 ++params;
-                if (wrptr(&bufp, &size, bytes) != 0)
-                    WARN_STATIC_BUFFER();
-                bytes = (int)snprintf(bufp, size, " [<%s> ...]", locp);
+                sbs_strcat(&sbs, " [<");
+                sbs_strcat(&sbs, locp);
+                sbs_strcat(&sbs, "> ...]");
             }
-            if (wrptr(&bufp, &size, bytes) != 0)
-                WARN_STATIC_BUFFER();
         }
         else if (cp == 's') {
             targetp = targets + 2;
             locp = LOC(lang, targetp->vars);
-            bytes = (int)snprintf(bufp, size, " <%s>", locp);
+            sbs_strcat(&sbs, " <");
+            sbs_strcat(&sbs, locp);
+            sbs_strcat(&sbs, ">");
             if (*params == '+') {
                 ++params;
-                if (wrptr(&bufp, &size, bytes) != 0)
-                    WARN_STATIC_BUFFER();
-                bytes = (int)snprintf(bufp, size, " [<%s> ...]", locp);
+                sbs_strcat(&sbs, " [<");
+                sbs_strcat(&sbs, locp);
+                sbs_strcat(&sbs, "> ...]");
             }
-            if (wrptr(&bufp, &size, bytes) != 0)
-                WARN_STATIC_BUFFER();
         }
         else if (cp == 'r') {
-            bytes = (int)str_strlcpy(bufp, " <x> <y>", size);
+            sbs_strcat(&sbs, " <x> <y>");
             if (*params == '+') {
                 ++params;
-                if (wrptr(&bufp, &size, bytes) != 0)
-                    WARN_STATIC_BUFFER();
-                bytes = (int)str_strlcpy(bufp, " [<x> <y> ...]", size);
+                sbs_strcat(&sbs, " [<x> <y> ...]");
             }
-            if (wrptr(&bufp, &size, bytes) != 0)
-                WARN_STATIC_BUFFER();
         }
         else if (cp == 'b') {
             targetp = targets + 3;
             locp = LOC(lang, targetp->vars);
-            bytes = (int)snprintf(bufp, size, " <%s>", locp);
+            sbs_strcat(&sbs, " <");
+            sbs_strcat(&sbs, locp);
+            sbs_strcat(&sbs, ">");
             if (*params == '+') {
                 ++params;
-                if (wrptr(&bufp, &size, bytes) != 0)
-                    WARN_STATIC_BUFFER();
-                bytes = (int)snprintf(bufp, size, " [<%s> ...]", locp);
+                sbs_strcat(&sbs, " [<");
+                sbs_strcat(&sbs, locp);
+                sbs_strcat(&sbs, "> ...]");
             }
-            if (wrptr(&bufp, &size, bytes) != 0)
-                WARN_STATIC_BUFFER();
         }
         else if (cp == 'k') {
             int i, maxparam = 0;
@@ -361,41 +361,35 @@ void nr_spell_syntax(struct stream *out, spellbook_entry * sbe, const struct loc
                     ++maxparam;
             }
             if (!maxparam || maxparam > 1) {
-                bytes = (int)str_strlcpy(bufp, " (", size);
-                if (wrptr(&bufp, &size, bytes) != 0)
-                    WARN_STATIC_BUFFER();
+                sbs_strcat(&sbs, " (");
             }
             i = 0;
             for (targetp = targets; targetp->flag; ++targetp) {
                 if (!maxparam || sp->sptyp & targetp->flag) {
                     if (i++ != 0) {
-                        bytes = (int)str_strlcpy(bufp, " |", size);
-                        if (wrptr(&bufp, &size, bytes) != 0)
-                            WARN_STATIC_BUFFER();
+                        sbs_strcat(&sbs, " |");
                     }
                     if (targetp->param && targetp->vars) {
                         locp = LOC(lang, targetp->vars);
-                        bytes =
-                            (int)snprintf(bufp, size, " %s <%s>", parameters[targetp->param],
-                                locp);
+                        sbs_strcat(&sbs, " ");
+                        sbs_strcat(&sbs, parameters[targetp->param]);
+                        sbs_strcat(&sbs, " <");
+                        sbs_strcat(&sbs, locp);
+                        sbs_strcat(&sbs, ">");
                         if (multi) {
-                            if (wrptr(&bufp, &size, bytes) != 0)
-                                WARN_STATIC_BUFFER();
-                            bytes = (int)snprintf(bufp, size, " [<%s> ...]", locp);
+                            sbs_strcat(&sbs, " [<");
+                            sbs_strcat(&sbs, locp);
+                            sbs_strcat(&sbs, "> ...]");
                         }
                     }
                     else {
-                        bytes =
-                            (int)snprintf(bufp, size, " %s", parameters[targetp->param]);
+                        sbs_strcat(&sbs, " ");
+                        sbs_strcat(&sbs, parameters[targetp->param]);
                     }
-                    if (wrptr(&bufp, &size, bytes) != 0)
-                        WARN_STATIC_BUFFER();
                 }
             }
             if (!maxparam || maxparam > 1) {
-                bytes = (int)str_strlcpy(bufp, " )", size);
-                if (wrptr(&bufp, &size, bytes) != 0)
-                    WARN_STATIC_BUFFER();
+                sbs_strcat(&sbs, " )");
             }
         }
         else if (cp == 'i' || cp == 'c') {
@@ -416,23 +410,24 @@ void nr_spell_syntax(struct stream *out, spellbook_entry * sbe, const struct loc
             }
             if (*params == '?') {
                 ++params;
-                bytes = (int)snprintf(bufp, size, " [<%s>]", locp);
+                sbs_strcat(&sbs, " [<");
+                sbs_strcat(&sbs, locp);
+                sbs_strcat(&sbs, ">]");
             }
             else {
-                bytes = (int)snprintf(bufp, size, " <%s>", locp);
+                sbs_strcat(&sbs, " <");
+                sbs_strcat(&sbs, locp);
+                sbs_strcat(&sbs, ">");
             }
-            if (wrptr(&bufp, &size, bytes) != 0)
-                WARN_STATIC_BUFFER();
         }
         else {
             log_error("unknown spell parameter %c for spell %s", cp, sp->sname);
         }
     }
-    *bufp = 0;
-    paragraph(out, buf, 2, 0, 0);
-
 }
 
+#include "util/bsdstring.h"
+
 void nr_spell(struct stream *out, spellbook_entry * sbe, const struct locale *lang)
 {
     int bytes, k, itemanz, costtyp;
@@ -542,10 +537,8 @@ void nr_spell(struct stream *out, spellbook_entry * sbe, const struct locale *la
     paragraph(out, buf, 0, 0, 0);
     paragraph(out, LOC(lang, "nr_spell_syntax"), 0, 0, 0);
 
-    bufp = buf;
-    size = sizeof(buf) - 1;
-
-    nr_spell_syntax(out, sbe, lang);
+    nr_spell_syntax(buf, sizeof(buf), sbe, lang);
+    paragraph(out, buf, 2, 0, 0);
 
     newline(out);
 }
diff --git a/src/report.h b/src/report.h
index 45c620562..bb3629497 100644
--- a/src/report.h
+++ b/src/report.h
@@ -30,7 +30,7 @@ extern "C" {
     void report_travelthru(struct stream *out, struct region * r, const struct faction * f);
     void report_region(struct stream *out, const struct region * r, struct faction * f);
 
-    void nr_spell_syntax(struct stream *out, struct spellbook_entry * sbe, const struct locale *lang);
+    void nr_spell_syntax(char *buf, size_t size, struct spellbook_entry * sbe, const struct locale *lang);
     void nr_spell(struct stream *out, struct spellbook_entry * sbe, const struct locale *lang);
 
 #ifdef __cplusplus
diff --git a/src/report.test.c b/src/report.test.c
index 77bb6dca6..4fa97825c 100644
--- a/src/report.test.c
+++ b/src/report.test.c
@@ -228,32 +228,11 @@ static void set_parameter(spell_fixture spell, char *value) {
 }
 
 static void check_spell_syntax(CuTest *tc, char *msg, spell_fixture *spell, char *syntax) {
-    stream strm;
     char buf[1024];
-    char *linestart, *newline;
-    size_t len;
 
-    mstream_init(&strm);
-    nr_spell_syntax(&strm, spell->sbe, spell->lang);
-    strm.api->rewind(strm.handle);
-    len = strm.api->read(strm.handle, buf, sizeof(buf));
-    buf[len] = '\0';
+    nr_spell_syntax(buf, sizeof(buf), spell->sbe, spell->lang);
 
-    linestart = strtok(buf, "\n");
-    while (linestart && !strstr(linestart, "ZAUBERE"))
-        linestart = strtok(NULL, "\n");
-
-    CuAssertPtrNotNull(tc, linestart);
-
-    newline = strtok(NULL, "\n");
-    while (newline) {
-        *(newline - 1) = '\n';
-        newline = strtok(NULL, "\n");
-    }
-
-    CuAssertStrEquals_Msg(tc, msg, syntax, linestart);
-
-    mstream_done(&strm);
+    CuAssertStrEquals_Msg(tc, msg, syntax, buf);
 }
 
 static void test_write_spell_syntax(CuTest *tc) {
@@ -262,54 +241,54 @@ static void test_write_spell_syntax(CuTest *tc) {
     test_setup();
     setup_spell_fixture(&spell);
 
-    check_spell_syntax(tc, "vanilla", &spell, "  ZAUBERE \"Testzauber\"");
+    check_spell_syntax(tc, "vanilla", &spell, "ZAUBERE Testzauber");
 
     spell.sp->sptyp |= FARCASTING;
-    check_spell_syntax(tc, "far", &spell, "  ZAUBERE [REGION x y] \"Testzauber\"");
+    check_spell_syntax(tc, "far", &spell, "ZAUBERE [REGION x y] Testzauber");
 
     spell.sp->sptyp |= SPELLLEVEL;
-    check_spell_syntax(tc, "farlevel", &spell, "  ZAUBERE [REGION x y] [STUFE n] \"Testzauber\"");
+    check_spell_syntax(tc, "farlevel", &spell, "ZAUBERE [REGION x y] [STUFE n] Testzauber");
     spell.sp->sptyp = 0;
 
     set_parameter(spell, "kc");
-    check_spell_syntax(tc, "kc", &spell, "  ZAUBERE \"Testzauber\" ( REGION | EINHEIT <enr> | SCHIFF <snr> | BURG <bnr> )");
+    check_spell_syntax(tc, "kc", &spell, "ZAUBERE Testzauber ( REGION | EINHEIT <enr> | SCHIFF <snr> | BURG <bnr> )");
 
     spell.sp->sptyp |= BUILDINGSPELL;
-    check_spell_syntax(tc, "kc typed", &spell, "  ZAUBERE \"Testzauber\" BURG <bnr>");
+    check_spell_syntax(tc, "kc typed", &spell, "ZAUBERE Testzauber BURG <bnr>");
     spell.sp->sptyp = 0;
 
     set_parameter(spell, "b");
-    check_spell_syntax(tc, "b", &spell, "  ZAUBERE \"Testzauber\" <bnr>");
+    check_spell_syntax(tc, "b", &spell, "ZAUBERE Testzauber <bnr>");
 
     set_parameter(spell, "s");
-    check_spell_syntax(tc, "s", &spell, "  ZAUBERE \"Testzauber\" <snr>");
+    check_spell_syntax(tc, "s", &spell, "ZAUBERE Testzauber <snr>");
 
     set_parameter(spell, "s+");
-    check_spell_syntax(tc, "s+", &spell, "  ZAUBERE \"Testzauber\" <snr> [<snr> ...]");
+    check_spell_syntax(tc, "s+", &spell, "ZAUBERE Testzauber <snr> [<snr> ...]");
 
     set_parameter(spell, "u");
-    check_spell_syntax(tc, "u", &spell, "  ZAUBERE \"Testzauber\" <enr>");
+    check_spell_syntax(tc, "u", &spell, "ZAUBERE Testzauber <enr>");
 
     set_parameter(spell, "r");
-    check_spell_syntax(tc, "r", &spell, "  ZAUBERE \"Testzauber\" <x> <y>");
+    check_spell_syntax(tc, "r", &spell, "ZAUBERE Testzauber <x> <y>");
 
     set_parameter(spell, "bc");
     free(spell.sp->syntax);
     spell.sp->syntax = str_strdup("hodor");
-    check_spell_syntax(tc, "bc hodor", &spell, "  ZAUBERE \"Testzauber\" <bnr> <Hodor>");
+    check_spell_syntax(tc, "bc hodor", &spell, "ZAUBERE Testzauber <bnr> <Hodor>");
     free(spell.sp->syntax);
     spell.sp->syntax = 0;
 
     set_parameter(spell, "c?");
     free(spell.sp->syntax);
     spell.sp->syntax = str_strdup("hodor");
-    check_spell_syntax(tc, "c?", &spell, "  ZAUBERE \"Testzauber\" [<Hodor>]");
+    check_spell_syntax(tc, "c?", &spell, "ZAUBERE Testzauber [<Hodor>]");
     free(spell.sp->syntax);
     spell.sp->syntax = 0;
 
     set_parameter(spell, "kc+");
     check_spell_syntax(tc, "kc+", &spell,
-        "  ZAUBERE \"Testzauber\" ( REGION | EINHEIT <enr> [<enr> ...] | SCHIFF <snr>\n  [<snr> ...] | BURG <bnr> [<bnr> ...] )");
+        "ZAUBERE Testzauber ( REGION | EINHEIT <enr> [<enr> ...] | SCHIFF <snr> [<snr> ...] | BURG <bnr> [<bnr> ...] )");
 
     cleanup_spell_fixture(&spell);
     test_teardown();

From 5b0f3f9ea712e63c4b3e85dce4001439aa5ea5f4 Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Sat, 9 Jun 2018 21:22:02 +0200
Subject: [PATCH 06/62] more sbstring upgrades

---
 src/report.c | 70 +++++++++++++++++++++-------------------------------
 1 file changed, 28 insertions(+), 42 deletions(-)

diff --git a/src/report.c b/src/report.c
index 55189a66c..c171eae5c 100644
--- a/src/report.c
+++ b/src/report.c
@@ -79,6 +79,7 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 /* util includes */
 #include <util/attrib.h>
 #include <util/base36.h>
+#include "util/bsdstring.h"
 #include <util/goodies.h>
 #include <util/language.h>
 #include <util/lists.h>
@@ -426,8 +427,6 @@ void nr_spell_syntax(char *buf, size_t size, spellbook_entry * sbe, const struct
     }
 }
 
-#include "util/bsdstring.h"
-
 void nr_spell(struct stream *out, spellbook_entry * sbe, const struct locale *lang)
 {
     int bytes, k, itemanz, costtyp;
@@ -1841,72 +1840,61 @@ nr_ship(struct stream *out, const region *r, const ship * sh, const faction * f,
 static void
 nr_building(struct stream *out, const region *r, const building *b, const faction *f)
 {
-    int i, bytes;
+    int i;
     const char *name, *bname, *billusion = NULL;
     const struct locale *lang;
-    char buffer[8192], *bufp = buffer;
+    char buffer[8192];
     message *msg;
-    size_t size = sizeof(buffer) - 1;
+    size_t size;
+    sbstring sbs;
 
     assert(f);
     lang = f->locale;
     newline(out);
-    bytes =
-        snprintf(bufp, size, "%s, %s %d, ", buildingname(b), LOC(lang,
-            "nr_size"), b->size);
-    if (wrptr(&bufp, &size, bytes) != 0)
-        WARN_STATIC_BUFFER();
+    sbs_init(&sbs, buffer, sizeof(buffer));
 
     report_building(b, &bname, &billusion);
+
+    size = str_slprintf(buffer, sizeof(buffer), "%s, %s %d, ", buildingname(b),
+        LOC(lang, "nr_size"), b->size);
+    sbs.end += size;
     name = LOC(lang, billusion ? billusion : bname);
-    bytes = (int)str_strlcpy(bufp, name, size);
-    if (wrptr(&bufp, &size, bytes) != 0)
-        WARN_STATIC_BUFFER();
+    sbs_strcat(&sbs, name);
+
     if (billusion) {
         unit *owner = building_owner(b);
         if (owner && owner->faction == f) {
             /* illusion. report real type */
-            name = LOC(lang, bname);
-            bytes = snprintf(bufp, size, " (%s)", name);
-            if (wrptr(&bufp, &size, bytes) != 0)
-                WARN_STATIC_BUFFER();
+            sbs_strcat(&sbs, " (");
+            sbs_strcat(&sbs, LOC(lang, bname));
+            sbs_strcat(&sbs, ")");
         }
     }
 
     if (!building_finished(b)) {
-        bytes = (int)str_strlcpy(bufp, " ", size);
-        if (wrptr(&bufp, &size, bytes) != 0)
-            WARN_STATIC_BUFFER();
-        bytes = (int)str_strlcpy(bufp, LOC(lang, "nr_building_inprogress"), size);
-        if (wrptr(&bufp, &size, bytes) != 0)
-            WARN_STATIC_BUFFER();
+        sbs_strcat(&sbs, " ");
+        sbs_strcat(&sbs, LOC(lang, "nr_building_inprogress"));
     }
 
     if (b->besieged > 0 && r->seen.mode >= seen_lighthouse) {
         msg = msg_message("nr_building_besieged", "soldiers diff", b->besieged,
             b->besieged - b->size * SIEGEFACTOR);
-        bytes = (int)nr_render(msg, lang, bufp, size, f);
-        if (wrptr(&bufp, &size, bytes) != 0)
-            WARN_STATIC_BUFFER();
+
+        size = nr_render(msg, lang, sbs.end, sbs.size - (sbs.end - sbs.begin), f);
+        sbs.end += size;
+
         msg_release(msg);
     }
     i = 0;
     if (b->display && b->display[0]) {
-        bytes = (int)str_strlcpy(bufp, "; ", size);
-        if (wrptr(&bufp, &size, bytes) != 0)
-            WARN_STATIC_BUFFER();
-        bytes = (int)str_strlcpy(bufp, b->display, size);
-        if (wrptr(&bufp, &size, bytes) != 0)
-            WARN_STATIC_BUFFER();
+        sbs_strcat(&sbs, "; ");
+        sbs_strcat(&sbs, b->display);
         i = b->display[strlen(b->display) - 1];
     }
 
     if (i != '!' && i != '?' && i != '.') {
-        bytes = (int)str_strlcpy(bufp, ".", size);
-        if (wrptr(&bufp, &size, bytes) != 0)
-            WARN_STATIC_BUFFER();
+        sbs_strcat(&sbs, ".");
     }
-    *bufp = 0;
     paragraph(out, buffer, 2, 0, 0);
 
     if (r->seen.mode >= seen_lighthouse) {
@@ -1916,14 +1904,10 @@ nr_building(struct stream *out, const region *r, const building *b, const factio
 
 static void nr_paragraph(struct stream *out, message * m, faction * f)
 {
-    int bytes;
-    char buf[4096], *bufp = buf;
-    size_t size = sizeof(buf) - 1;
+    char buf[4096];
 
     assert(f);
-    bytes = (int)nr_render(m, f->locale, bufp, size, f);
-    if (wrptr(&bufp, &size, bytes) != 0)
-        WARN_STATIC_BUFFER();
+    nr_render(m, f->locale, buf, sizeof(buf), f);
     msg_release(m);
 
     paragraph(out, buf, 0, 0, 0);
@@ -2027,6 +2011,8 @@ void report_travelthru(struct stream *out, region *r, const faction *f)
     }
 }
 
+#include "util/bsdstring.h"
+
 int
 report_plaintext(const char *filename, report_context * ctx,
     const char *bom)

From e168c667ade06ce4a5a12fd23d7cecba45e8590b Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno@kn-bremen.de>
Date: Sun, 3 Jun 2018 21:02:59 +0200
Subject: [PATCH 07/62] make orders-accept use mutt to send email, fix encoding

---
 process/accept-orders.py | 377 ++++++++++++++++++++++++++++++++++++++
 process/orders-accept    | 379 +--------------------------------------
 process/orders-accept.py | 378 ++++++++++++++++++++++++++++++++++++++
 3 files changed, 757 insertions(+), 377 deletions(-)
 create mode 100755 process/accept-orders.py
 create mode 100755 process/orders-accept.py

diff --git a/process/accept-orders.py b/process/accept-orders.py
new file mode 100755
index 000000000..3bf9ea965
--- /dev/null
+++ b/process/accept-orders.py
@@ -0,0 +1,377 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from email.Utils import parseaddr
+from email.Parser import Parser
+import os
+import os.path
+import ConfigParser
+from re import compile, IGNORECASE
+from stat import ST_MTIME
+from string import upper, split, replace
+import logging
+import sys
+import subprocess
+from sys import stdin
+from time import ctime, sleep, time
+from socket import gethostname
+from rfc822 import parsedate_tz, mktime_tz
+
+if 'ERESSEA' in os.environ:
+    dir = os.environ['ERESSEA']
+elif 'HOME' in os.environ:
+    dir = os.path.join(os.environ['HOME'], 'eressea')
+else: # WTF? No HOME?
+    dir = "/home/eressea/eressea"
+if not os.path.isdir(dir):
+    print "please set the ERESSEA environment variable to the install path"
+    sys.exit(1)
+rootdir = dir
+
+game = int(sys.argv[1])
+gamedir = os.path.join(rootdir, "game-%d" % (game, ))
+frommail = 'eressea-server@kn-bremen.de'
+gamename = 'Eressea'
+sender = '%s Server <%s>' % (gamename, frommail)
+
+inifile = os.path.join(gamedir, 'eressea.ini')
+if not os.path.exists(inifile):
+    print "no such file: " . inifile
+else:
+    config = ConfigParser.ConfigParser()
+    config.read(inifile)
+    if config.has_option('game', 'email'):
+        frommail = config.get('game', 'email')
+    if config.has_option('game', 'name'):
+        gamename = config.get('game', 'name')
+    if config.has_option('game', 'sender'):
+        sender = config.get('game', 'sender')
+    else:
+        sender = "%s Server <%s>" % (gamename, frommail)
+    config = None
+prefix = 'turn-'
+hostname = gethostname()
+orderbase = "orders.dir"
+sendmail = True
+# maximum number of reports per sender:
+maxfiles = 20
+# write headers to file?
+writeheaders = True
+# reject all html email?
+rejecthtml = True
+
+def unlock_file(filename):
+    try:
+        os.unlink(filename+".lock")
+    except:
+        print "could not unlock %s.lock, file not found" % filename
+
+def lock_file(filename):
+    i = 0
+    wait = 1
+    if not os.path.exists(filename):
+        file=open(filename, "w")
+        file.close()
+    while True:
+        try:
+            os.symlink(filename, filename+".lock")
+            return
+        except:
+            i = i+1
+            if i == 5: unlock_file(filename)
+            sleep(wait)
+            wait = wait*2
+
+messages = {
+        "multipart-en" :
+                "ERROR: The orders you sent contain no plaintext. " \
+                "The Eressea server cannot process orders containing HTML " \
+                "or invalid attachments, which are the reasons why this " \
+                "usually happens. Please change the settings of your mail " \
+                "software and re-send the orders.",
+
+        "multipart-de" :
+                "FEHLER: Die von dir eingeschickte Mail enthält keinen " \
+                "Text. Evtl. hast Du den Zug als HTML oder als anderweitig " \
+                "ungültig formatierte Mail ingeschickt. Wir können ihn " \
+                "deshalb nicht berücksichtigen. Schicke den Zug nochmals " \
+                "als reinen Text ohne Formatierungen ein.",
+
+        "maildate-de":
+                "Es erreichte uns bereits ein Zug mit einem späteren " \
+                "Absendedatum (%s > %s). Entweder ist deine " \
+                "Systemzeit verstellt, oder ein Zug hat einen anderen Zug von " \
+                "dir auf dem Transportweg überholt. Entscheidend für die " \
+                "Auswertungsreihenfolge ist das Absendedatum, d.h. der Date:-Header " \
+                "deiner Mail.",
+
+        "maildate-en":
+                "The server already received an order file that was sent at a later " \
+                "date (%s > %s). Either your system clock is wrong, or two messages have " \
+                "overtaken each other on the way to the server. The order of " \
+                "execution on the server is always according to the Date: header in " \
+                "your mail.",
+
+        "nodate-en":
+                "Your message did not contain a valid Date: header in accordance with RFC2822.",
+
+        "nodate-de":
+                "Deine Nachricht enthielt keinen gueltigen Date: header nach RFC2822.",
+
+        "error-de":
+                "Fehler",
+
+        "error-en":
+                "Error",
+
+        "warning-de":
+                "Warnung",
+
+        "warning-en":
+                "Warning",
+
+        "subject-de":
+                "Befehle angekommen",
+
+        "subject-en":
+                "orders received"
+}
+
+# return 1 if addr is a valid email address
+def valid_email(addr):
+    rfc822_specials = '/()<>@,;:\\"[]'
+    # First we validate the name portion (name@domain)
+    c = 0
+    while c < len(addr):
+        if addr[c] == '"' and (not c or addr[c - 1] == '.' or addr[c - 1] == '"'):
+            c = c + 1
+            while c < len(addr):
+                if addr[c] == '"': break
+                if addr[c] == '\\' and addr[c + 1] == ' ':
+                    c = c + 2
+                    continue
+                if ord(addr[c]) < 32 or ord(addr[c]) >= 127: return 0
+                c = c + 1
+            else: return 0
+            if addr[c] == '@': break
+            if addr[c] != '.': return 0
+            c = c + 1
+            continue
+        if addr[c] == '@': break
+        if ord(addr[c]) <= 32 or ord(addr[c]) >= 127: return 0
+        if addr[c] in rfc822_specials: return 0
+        c = c + 1
+    if not c or addr[c - 1] == '.': return 0
+
+    # Next we validate the domain portion (name@domain)
+    domain = c = c + 1
+    if domain >= len(addr): return 0
+    count = 0
+    while c < len(addr):
+        if addr[c] == '.':
+            if c == domain or addr[c - 1] == '.': return 0
+            count = count + 1
+        if ord(addr[c]) <= 32 or ord(addr[c]) >= 127: return 0
+        if addr[c] in rfc822_specials: return 0
+        c = c + 1
+    return count >= 1
+
+# return the replyto or from address in the header
+def get_sender(header):
+    replyto = header.get("Reply-To")
+    if replyto is None:
+        replyto = header.get("From")
+        if replyto is None: return None
+    x = parseaddr(replyto)
+    return x[1]
+
+# return first available filename basename,[0-9]+
+def available_file(dirname, basename):
+    ver = 0
+    maxdate = 0
+    filename = "%s/%s,%s,%d" % (dirname, basename, hostname, ver)
+    while os.path.exists(filename):
+        maxdate = max(os.stat(filename)[ST_MTIME], maxdate)
+        ver = ver + 1
+        filename = "%s/%s,%s,%d" % (dirname, basename, hostname, ver)
+    if ver >= maxfiles:
+        return None, None
+    return maxdate, filename
+
+def formatpar(string, l=76, indent=2):
+    words = split(string)
+    res = ""
+    ll = 0
+    first = 1
+
+    for word in words:
+        if first == 1:
+            res = word
+            first = 0
+            ll = len(word)
+        else:
+            if ll + len(word) > l:
+                res = res + "\n"+" "*indent+word
+                ll = len(word) + indent
+            else:
+                res = res+" "+word
+                ll = ll + len(word) + 1
+
+    return res+"\n"
+
+def store_message(message, filename):
+    outfile = open(filename, "w")
+    outfile.write(message.as_string())
+    outfile.close()
+    return
+
+def write_part(outfile, part):
+    charset = part.get_content_charset()
+    payload = part.get_payload(decode=True)
+
+    if charset is None:
+        charset = "latin1"
+    try:
+        msg = payload.decode(charset, "ignore")
+    except:
+        msg = payload
+        charset = None
+    try:
+        utf8 = msg.encode("utf-8", "ignore")
+        outfile.write(utf8)
+    except:
+        outfile.write(msg)
+        return False
+    outfile.write("\n");
+    return True
+
+def copy_orders(message, filename, sender):
+        # print the header first
+    if writeheaders:
+        from os.path import split
+        dirname, basename = split(filename)
+        dirname = dirname + '/headers'
+        if not os.path.exists(dirname): os.mkdir(dirname)
+        outfile = open(dirname + '/' + basename, "w")
+        for name, value in message.items():
+            outfile.write(name + ": " + value + "\n")
+        outfile.close()
+
+    found = False
+    outfile = open(filename, "w")
+    if message.is_multipart():
+        for part in message.get_payload():
+            content_type = part.get_content_type()
+            logger.debug("found content type %s for %s" % (content_type, sender))
+            if content_type=="text/plain":
+                if write_part(outfile, part):
+                    found = True
+                else:
+                    charset = part.get_content_charset()
+                    logger.error("could not write text/plain part (charset=%s) for %s" % (charset, sender))
+
+    else:
+        if write_part(outfile, message):
+            found = True
+        else:
+            charset = message.get_content_charset()
+            logger.error("could not write text/plain message (charset=%s) for %s" % (charset, sender))
+    outfile.close()
+    return found
+
+# create a file, containing:
+# game=0 locale=de file=/path/to/filename email=rcpt@domain.to
+def accept(game, locale, stream, extend=None):
+    global rootdir, orderbase, gamedir, gamename, sender
+    if extend is not None:
+        orderbase = orderbase + ".pre-" + extend
+    savedir = os.path.join(gamedir, orderbase)
+    # check if it's one of the pre-sent orders.
+    # create the save-directories if they don't exist
+    if not os.path.exists(gamedir): os.mkdir(gamedir)
+    if not os.path.exists(savedir): os.mkdir(savedir)
+    # parse message
+    message = Parser().parse(stream)
+    email = get_sender(message)
+    logger = logging.getLogger(email)
+    # write syslog
+    if email is None or valid_email(email)==0:
+        logger.warning("invalid email address: " + str(email))
+        return -1
+    logger.info("received orders from " + email)
+    # get an available filename
+    lock_file(gamedir + "/orders.queue")
+    maxdate, filename = available_file(savedir, prefix + email)
+    if filename is None:
+        logger.warning("more than " + str(maxfiles) + " orders from " + email)
+        return -1
+    # copy the orders to the file
+    text_ok = copy_orders(message, filename, email)
+    unlock_file(gamedir + "/orders.queue")
+
+    warning, msg, fail = None, "", False
+    maildate = message.get("Date")
+    if maildate != None:
+        turndate = mktime_tz(parsedate_tz(maildate))
+        os.utime(filename, (turndate, turndate))
+        logger.debug("mail date is '%s' (%d)" % (maildate, turndate))
+        if False and turndate < maxdate:
+            logger.warning("inconsistent message date " + email)
+            warning = " (" + messages["warning-" + locale] + ")"
+            msg = msg + formatpar(messages["maildate-" + locale] % (ctime(maxdate),ctime(turndate)), 76, 2) + "\n"
+    else:
+        logger.warning("missing message date " + email)
+        warning = " (" + messages["warning-" + locale] + ")"
+        msg = msg + formatpar(messages["nodate-" + locale], 76, 2) + "\n"
+
+    if not text_ok:
+        warning = " (" + messages["error-" + locale] + ")"
+        msg = msg + formatpar(messages["multipart-" + locale], 76, 2) + "\n"
+        logger.warning("rejected - no text/plain in orders from " + email)
+        os.unlink(filename)
+        savedir = savedir + "/rejected"
+        if not os.path.exists(savedir): os.mkdir(savedir)
+        lock_file(gamedir + "/orders.queue")
+        maxdate, filename = available_file(savedir, prefix + email)
+        store_message(message, filename)
+        unlock_file(gamedir + "/orders.queue")
+        fail = True
+
+    if sendmail and warning is not None:
+        subject = gamename + " " + messages["subject-"+locale] + warning
+        print("mail " + subject)
+        ps = subprocess.Popen(['mutt', '-s', subject, email], stdin=subprocess.PIPE)
+        ps.communicate(msg)
+
+    if not sendmail:
+        print text_ok, fail, email
+        print filename
+
+    if not fail:
+        lock_file(gamedir + "/orders.queue")
+        queue = open(gamedir + "/orders.queue", "a")
+        queue.write("email=%s file=%s locale=%s game=%s\n" % (email, filename, locale, game))
+        queue.close()
+        unlock_file(gamedir + "/orders.queue")
+
+    logger.info("done - accepted orders from " + email)
+
+    return 0
+
+# the main body of the script:
+try:
+    os.mkdir(os.path.join(rootdir, 'log'))
+except:
+    pass # already exists?
+LOG_FILENAME=os.path.join(rootdir, 'log/orders.log')
+logging.basicConfig(level=logging.DEBUG, filename=LOG_FILENAME)
+logger = logging
+delay = None # TODO: parse the turn delay
+locale = sys.argv[2]
+infile = stdin
+if len(sys.argv)>3:
+    infile = open(sys.argv[3], "r")
+retval = accept(game, locale, infile, delay)
+if infile!=stdin:
+    infile.close()
+sys.exit(retval)
diff --git a/process/orders-accept b/process/orders-accept
index 2f8f0bd29..bd8ae523f 100755
--- a/process/orders-accept
+++ b/process/orders-accept
@@ -1,378 +1,3 @@
-#!/usr/bin/env python
-# -*- coding: iso-8859-1 -*-
+#!/bin/sh
+python accept-orders.py "$@"
 
-from email.Utils import parseaddr
-from email.Parser import Parser
-import os
-import os.path
-import ConfigParser
-from re import compile, IGNORECASE
-from stat import ST_MTIME
-from string import upper, split, replace
-import logging
-import sys
-from sys import stdin
-from time import ctime, sleep, time
-from socket import gethostname
-from rfc822 import parsedate_tz, mktime_tz
-
-if 'ERESSEA' in os.environ:
-    dir = os.environ['ERESSEA']
-elif 'HOME' in os.environ:
-    dir = os.path.join(os.environ['HOME'], '/eressea')
-else: # WTF? No HOME?
-    dir = "/home/eressea/eressea"
-if not os.path.isdir(dir):
-    print "please set the ERESSEA environment variable to the install path"
-    sys.exit(1)
-rootdir = dir
-
-game = int(sys.argv[1])
-gamedir = os.path.join(rootdir, "game-%d" % (game, ))
-frommail = 'eressea-server@kn-bremen.de'
-gamename = 'Eressea'
-sender = '%s Server <%s>' % (gamename, frommail)
-
-inifile = os.path.join(gamedir, 'eressea.ini')
-if not os.path.exists(inifile):
-    print "no such file: " . inifile
-else:
-    config = ConfigParser.ConfigParser()
-    config.read(inifile)
-    if config.has_option('game', 'email'):
-        frommail = config.get('game', 'email')
-    if config.has_option('game', 'name'):
-        gamename = config.get('game', 'name')
-    if config.has_option('game', 'sender'):
-        sender = config.get('game', 'sender')
-    else:
-        sender = "%s Server <%s>" % (gamename, frommail)
-    config = None
-prefix = 'turn-'
-hostname = gethostname()
-orderbase = "orders.dir"
-sendmail = True
-# maximum number of reports per sender:
-maxfiles = 20
-# write headers to file?
-writeheaders = True
-# reject all html email?
-rejecthtml = True
-
-def unlock_file(filename):
-    try:
-        os.unlink(filename+".lock")
-    except:
-        print "could not unlock %s.lock, file not found" % filename
-
-def lock_file(filename):
-    i = 0
-    wait = 1
-    if not os.path.exists(filename):
-        file=open(filename, "w")
-        file.close()
-    while True:
-        try:
-            os.symlink(filename, filename+".lock")
-            return
-        except:
-            i = i+1
-            if i == 5: unlock_file(filename)
-            sleep(wait)
-            wait = wait*2
-
-messages = {
-        "multipart-en" :
-                "ERROR: The orders you sent contain no plaintext. " \
-                "The Eressea server cannot process orders containing HTML " \
-                "or invalid attachments, which are the reasons why this " \
-                "usually happens. Please change the settings of your mail " \
-                "software and re-send the orders.",
-
-        "multipart-de" :
-                "FEHLER: Die von dir eingeschickte Mail enth�lt keinen " \
-                "Text. Evtl. hast Du den Zug als HTML oder als anderweitig " \
-                "ung�ltig formatierte Mail ingeschickt. Wir k�nnen ihn " \
-                "deshalb nicht ber�cksichtigen. Schicke den Zug nochmals " \
-                "als reinen Text ohne Formatierungen ein.",
-
-        "maildate-de":
-                "Es erreichte uns bereits ein Zug mit einem sp�teren " \
-                "Absendedatum (%s > %s). Entweder ist deine " \
-                "Systemzeit verstellt, oder ein Zug hat einen anderen Zug von " \
-                "dir auf dem Transportweg �berholt. Entscheidend f�r die " \
-                "Auswertungsreihenfolge ist das Absendedatum, d.h. der Date:-Header " \
-                "deiner Mail.",
-
-        "maildate-en":
-                "The server already received an order file that was sent at a later " \
-                "date (%s > %s). Either your system clock is wrong, or two messages have " \
-                "overtaken each other on the way to the server. The order of " \
-                "execution on the server is always according to the Date: header in " \
-                "your mail.",
-
-        "nodate-en":
-                "Your message did not contain a valid Date: header in accordance with RFC2822.",
-
-        "nodate-de":
-                "Deine Nachricht enthielt keinen gueltigen Date: header nach RFC2822.",
-
-        "error-de":
-                "Fehler",
-
-        "error-en":
-                "Error",
-
-        "warning-de":
-                "Warnung",
-
-        "warning-en":
-                "Warning",
-
-        "subject-de":
-                "Befehle angekommen",
-
-        "subject-en":
-                "orders received"
-}
-
-# return 1 if addr is a valid email address
-def valid_email(addr):
-    rfc822_specials = '/()<>@,;:\\"[]'
-    # First we validate the name portion (name@domain)
-    c = 0
-    while c < len(addr):
-        if addr[c] == '"' and (not c or addr[c - 1] == '.' or addr[c - 1] == '"'):
-            c = c + 1
-            while c < len(addr):
-                if addr[c] == '"': break
-                if addr[c] == '\\' and addr[c + 1] == ' ':
-                    c = c + 2
-                    continue
-                if ord(addr[c]) < 32 or ord(addr[c]) >= 127: return 0
-                c = c + 1
-            else: return 0
-            if addr[c] == '@': break
-            if addr[c] != '.': return 0
-            c = c + 1
-            continue
-        if addr[c] == '@': break
-        if ord(addr[c]) <= 32 or ord(addr[c]) >= 127: return 0
-        if addr[c] in rfc822_specials: return 0
-        c = c + 1
-    if not c or addr[c - 1] == '.': return 0
-
-    # Next we validate the domain portion (name@domain)
-    domain = c = c + 1
-    if domain >= len(addr): return 0
-    count = 0
-    while c < len(addr):
-        if addr[c] == '.':
-            if c == domain or addr[c - 1] == '.': return 0
-            count = count + 1
-        if ord(addr[c]) <= 32 or ord(addr[c]) >= 127: return 0
-        if addr[c] in rfc822_specials: return 0
-        c = c + 1
-    return count >= 1
-
-# return the replyto or from address in the header
-def get_sender(header):
-    replyto = header.get("Reply-To")
-    if replyto is None:
-        replyto = header.get("From")
-        if replyto is None: return None
-    x = parseaddr(replyto)
-    return x[1]
-
-# return first available filename basename,[0-9]+
-def available_file(dirname, basename):
-    ver = 0
-    maxdate = 0
-    filename = "%s/%s,%s,%d" % (dirname, basename, hostname, ver)
-    while os.path.exists(filename):
-        maxdate = max(os.stat(filename)[ST_MTIME], maxdate)
-        ver = ver + 1
-        filename = "%s/%s,%s,%d" % (dirname, basename, hostname, ver)
-    if ver >= maxfiles:
-        return None, None
-    return maxdate, filename
-
-def formatpar(string, l=76, indent=2):
-    words = split(string)
-    res = ""
-    ll = 0
-    first = 1
-
-    for word in words:
-        if first == 1:
-            res = word
-            first = 0
-            ll = len(word)
-        else:
-            if ll + len(word) > l:
-                res = res + "\n"+" "*indent+word
-                ll = len(word) + indent
-            else:
-                res = res+" "+word
-                ll = ll + len(word) + 1
-
-    return res+"\n"
-
-def store_message(message, filename):
-    outfile = open(filename, "w")
-    outfile.write(message.as_string())
-    outfile.close()
-    return
-
-def write_part(outfile, part):
-    charset = part.get_content_charset()
-    payload = part.get_payload(decode=True)
-
-    if charset is None:
-        charset = "latin1"
-    try:
-        msg = payload.decode(charset, "ignore")
-    except:
-        msg = payload
-        charset = None
-    try:
-        utf8 = msg.encode("utf-8", "ignore")
-        outfile.write(utf8)
-    except:
-        outfile.write(msg)
-        return False
-    outfile.write("\n");
-    return True
-
-def copy_orders(message, filename, sender):
-        # print the header first
-    if writeheaders:
-        from os.path import split
-        dirname, basename = split(filename)
-        dirname = dirname + '/headers'
-        if not os.path.exists(dirname): os.mkdir(dirname)
-        outfile = open(dirname + '/' + basename, "w")
-        for name, value in message.items():
-            outfile.write(name + ": " + value + "\n")
-        outfile.close()
-
-    found = False
-    outfile = open(filename, "w")
-    if message.is_multipart():
-        for part in message.get_payload():
-            content_type = part.get_content_type()
-            logger.debug("found content type %s for %s" % (content_type, sender))
-            if content_type=="text/plain":
-                if write_part(outfile, part):
-                    found = True
-                else:
-                    charset = part.get_content_charset()
-                    logger.error("could not write text/plain part (charset=%s) for %s" % (charset, sender))
-
-    else:
-        if write_part(outfile, message):
-            found = True
-        else:
-            charset = message.get_content_charset()
-            logger.error("could not write text/plain message (charset=%s) for %s" % (charset, sender))
-    outfile.close()
-    return found
-
-# create a file, containing:
-# game=0 locale=de file=/path/to/filename email=rcpt@domain.to
-def accept(game, locale, stream, extend=None):
-    global rootdir, orderbase, gamedir, gamename, sender
-    if extend is not None:
-        orderbase = orderbase + ".pre-" + extend
-    savedir = os.path.join(gamedir, orderbase)
-    # check if it's one of the pre-sent orders.
-    # create the save-directories if they don't exist
-    if not os.path.exists(gamedir): os.mkdir(gamedir)
-    if not os.path.exists(savedir): os.mkdir(savedir)
-    # parse message
-    message = Parser().parse(stream)
-    email = get_sender(message)
-    logger = logging.getLogger(email)
-    # write syslog
-    if email is None or valid_email(email)==0:
-        logger.warning("invalid email address: " + str(email))
-        return -1
-    logger.info("received orders from " + email)
-    # get an available filename
-    lock_file(gamedir + "/orders.queue")
-    maxdate, filename = available_file(savedir, prefix + email)
-    if filename is None:
-        logger.warning("more than " + str(maxfiles) + " orders from " + email)
-        return -1
-    # copy the orders to the file
-    text_ok = copy_orders(message, filename, email)
-    unlock_file(gamedir + "/orders.queue")
-
-    warning, msg, fail = None, "", False
-    maildate = message.get("Date")
-    if maildate != None:
-        turndate = mktime_tz(parsedate_tz(maildate))
-        os.utime(filename, (turndate, turndate))
-        logger.debug("mail date is '%s' (%d)" % (maildate, turndate))
-        if turndate < maxdate:
-            logger.warning("inconsistent message date " + email)
-            warning = " (" + messages["warning-" + locale] + ")"
-            msg = msg + formatpar(messages["maildate-" + locale] % (ctime(maxdate),ctime(turndate)), 76, 2) + "\n"
-    else:
-        logger.warning("missing message date " + email)
-        warning = " (" + messages["warning-" + locale] + ")"
-        msg = msg + formatpar(messages["nodate-" + locale], 76, 2) + "\n"
-
-    if not text_ok:
-        warning = " (" + messages["error-" + locale] + ")"
-        msg = msg + formatpar(messages["multipart-" + locale], 76, 2) + "\n"
-        logger.warning("rejected - no text/plain in orders from " + email)
-        os.unlink(filename)
-        savedir = savedir + "/rejected"
-        if not os.path.exists(savedir): os.mkdir(savedir)
-        lock_file(gamedir + "/orders.queue")
-        maxdate, filename = available_file(savedir, prefix + email)
-        store_message(message, filename)
-        unlock_file(gamedir + "/orders.queue")
-        fail = True
-
-    if sendmail and warning is not None:
-        subject = gamename + " " + messages["subject-"+locale] + warning
-        mail = "Subject: %s\nFrom: %s\nTo: %s\n\n" % (subject, sender, email) + msg
-        from smtplib import SMTP
-        server = SMTP("localhost")
-        server.sendmail(sender, email, mail)
-        server.close()
-
-    if not sendmail:
-        print text_ok, fail, email
-        print filename
-
-    if not fail:
-        lock_file(gamedir + "/orders.queue")
-        queue = open(gamedir + "/orders.queue", "a")
-        queue.write("email=%s file=%s locale=%s game=%s\n" % (email, filename, locale, game))
-        queue.close()
-        unlock_file(gamedir + "/orders.queue")
-
-    logger.info("done - accepted orders from " + email)
-
-    return 0
-
-# the main body of the script:
-try:
-    os.mkdir(os.path.join(rootdir, 'log'))
-except:
-    pass # already exists?
-LOG_FILENAME=os.path.join(rootdir, 'log/orders.log')
-logging.basicConfig(level=logging.DEBUG, filename=LOG_FILENAME)
-logger = logging
-delay=None # TODO: parse the turn delay
-locale = sys.argv[2]
-infile = stdin
-if len(sys.argv)>3:
-    infile = open(sys.argv[3], "r")
-retval = accept(game, locale, infile, delay)
-if infile!=stdin:
-    infile.close()
-sys.exit(retval)
diff --git a/process/orders-accept.py b/process/orders-accept.py
new file mode 100755
index 000000000..2f8f0bd29
--- /dev/null
+++ b/process/orders-accept.py
@@ -0,0 +1,378 @@
+#!/usr/bin/env python
+# -*- coding: iso-8859-1 -*-
+
+from email.Utils import parseaddr
+from email.Parser import Parser
+import os
+import os.path
+import ConfigParser
+from re import compile, IGNORECASE
+from stat import ST_MTIME
+from string import upper, split, replace
+import logging
+import sys
+from sys import stdin
+from time import ctime, sleep, time
+from socket import gethostname
+from rfc822 import parsedate_tz, mktime_tz
+
+if 'ERESSEA' in os.environ:
+    dir = os.environ['ERESSEA']
+elif 'HOME' in os.environ:
+    dir = os.path.join(os.environ['HOME'], '/eressea')
+else: # WTF? No HOME?
+    dir = "/home/eressea/eressea"
+if not os.path.isdir(dir):
+    print "please set the ERESSEA environment variable to the install path"
+    sys.exit(1)
+rootdir = dir
+
+game = int(sys.argv[1])
+gamedir = os.path.join(rootdir, "game-%d" % (game, ))
+frommail = 'eressea-server@kn-bremen.de'
+gamename = 'Eressea'
+sender = '%s Server <%s>' % (gamename, frommail)
+
+inifile = os.path.join(gamedir, 'eressea.ini')
+if not os.path.exists(inifile):
+    print "no such file: " . inifile
+else:
+    config = ConfigParser.ConfigParser()
+    config.read(inifile)
+    if config.has_option('game', 'email'):
+        frommail = config.get('game', 'email')
+    if config.has_option('game', 'name'):
+        gamename = config.get('game', 'name')
+    if config.has_option('game', 'sender'):
+        sender = config.get('game', 'sender')
+    else:
+        sender = "%s Server <%s>" % (gamename, frommail)
+    config = None
+prefix = 'turn-'
+hostname = gethostname()
+orderbase = "orders.dir"
+sendmail = True
+# maximum number of reports per sender:
+maxfiles = 20
+# write headers to file?
+writeheaders = True
+# reject all html email?
+rejecthtml = True
+
+def unlock_file(filename):
+    try:
+        os.unlink(filename+".lock")
+    except:
+        print "could not unlock %s.lock, file not found" % filename
+
+def lock_file(filename):
+    i = 0
+    wait = 1
+    if not os.path.exists(filename):
+        file=open(filename, "w")
+        file.close()
+    while True:
+        try:
+            os.symlink(filename, filename+".lock")
+            return
+        except:
+            i = i+1
+            if i == 5: unlock_file(filename)
+            sleep(wait)
+            wait = wait*2
+
+messages = {
+        "multipart-en" :
+                "ERROR: The orders you sent contain no plaintext. " \
+                "The Eressea server cannot process orders containing HTML " \
+                "or invalid attachments, which are the reasons why this " \
+                "usually happens. Please change the settings of your mail " \
+                "software and re-send the orders.",
+
+        "multipart-de" :
+                "FEHLER: Die von dir eingeschickte Mail enth�lt keinen " \
+                "Text. Evtl. hast Du den Zug als HTML oder als anderweitig " \
+                "ung�ltig formatierte Mail ingeschickt. Wir k�nnen ihn " \
+                "deshalb nicht ber�cksichtigen. Schicke den Zug nochmals " \
+                "als reinen Text ohne Formatierungen ein.",
+
+        "maildate-de":
+                "Es erreichte uns bereits ein Zug mit einem sp�teren " \
+                "Absendedatum (%s > %s). Entweder ist deine " \
+                "Systemzeit verstellt, oder ein Zug hat einen anderen Zug von " \
+                "dir auf dem Transportweg �berholt. Entscheidend f�r die " \
+                "Auswertungsreihenfolge ist das Absendedatum, d.h. der Date:-Header " \
+                "deiner Mail.",
+
+        "maildate-en":
+                "The server already received an order file that was sent at a later " \
+                "date (%s > %s). Either your system clock is wrong, or two messages have " \
+                "overtaken each other on the way to the server. The order of " \
+                "execution on the server is always according to the Date: header in " \
+                "your mail.",
+
+        "nodate-en":
+                "Your message did not contain a valid Date: header in accordance with RFC2822.",
+
+        "nodate-de":
+                "Deine Nachricht enthielt keinen gueltigen Date: header nach RFC2822.",
+
+        "error-de":
+                "Fehler",
+
+        "error-en":
+                "Error",
+
+        "warning-de":
+                "Warnung",
+
+        "warning-en":
+                "Warning",
+
+        "subject-de":
+                "Befehle angekommen",
+
+        "subject-en":
+                "orders received"
+}
+
+# return 1 if addr is a valid email address
+def valid_email(addr):
+    rfc822_specials = '/()<>@,;:\\"[]'
+    # First we validate the name portion (name@domain)
+    c = 0
+    while c < len(addr):
+        if addr[c] == '"' and (not c or addr[c - 1] == '.' or addr[c - 1] == '"'):
+            c = c + 1
+            while c < len(addr):
+                if addr[c] == '"': break
+                if addr[c] == '\\' and addr[c + 1] == ' ':
+                    c = c + 2
+                    continue
+                if ord(addr[c]) < 32 or ord(addr[c]) >= 127: return 0
+                c = c + 1
+            else: return 0
+            if addr[c] == '@': break
+            if addr[c] != '.': return 0
+            c = c + 1
+            continue
+        if addr[c] == '@': break
+        if ord(addr[c]) <= 32 or ord(addr[c]) >= 127: return 0
+        if addr[c] in rfc822_specials: return 0
+        c = c + 1
+    if not c or addr[c - 1] == '.': return 0
+
+    # Next we validate the domain portion (name@domain)
+    domain = c = c + 1
+    if domain >= len(addr): return 0
+    count = 0
+    while c < len(addr):
+        if addr[c] == '.':
+            if c == domain or addr[c - 1] == '.': return 0
+            count = count + 1
+        if ord(addr[c]) <= 32 or ord(addr[c]) >= 127: return 0
+        if addr[c] in rfc822_specials: return 0
+        c = c + 1
+    return count >= 1
+
+# return the replyto or from address in the header
+def get_sender(header):
+    replyto = header.get("Reply-To")
+    if replyto is None:
+        replyto = header.get("From")
+        if replyto is None: return None
+    x = parseaddr(replyto)
+    return x[1]
+
+# return first available filename basename,[0-9]+
+def available_file(dirname, basename):
+    ver = 0
+    maxdate = 0
+    filename = "%s/%s,%s,%d" % (dirname, basename, hostname, ver)
+    while os.path.exists(filename):
+        maxdate = max(os.stat(filename)[ST_MTIME], maxdate)
+        ver = ver + 1
+        filename = "%s/%s,%s,%d" % (dirname, basename, hostname, ver)
+    if ver >= maxfiles:
+        return None, None
+    return maxdate, filename
+
+def formatpar(string, l=76, indent=2):
+    words = split(string)
+    res = ""
+    ll = 0
+    first = 1
+
+    for word in words:
+        if first == 1:
+            res = word
+            first = 0
+            ll = len(word)
+        else:
+            if ll + len(word) > l:
+                res = res + "\n"+" "*indent+word
+                ll = len(word) + indent
+            else:
+                res = res+" "+word
+                ll = ll + len(word) + 1
+
+    return res+"\n"
+
+def store_message(message, filename):
+    outfile = open(filename, "w")
+    outfile.write(message.as_string())
+    outfile.close()
+    return
+
+def write_part(outfile, part):
+    charset = part.get_content_charset()
+    payload = part.get_payload(decode=True)
+
+    if charset is None:
+        charset = "latin1"
+    try:
+        msg = payload.decode(charset, "ignore")
+    except:
+        msg = payload
+        charset = None
+    try:
+        utf8 = msg.encode("utf-8", "ignore")
+        outfile.write(utf8)
+    except:
+        outfile.write(msg)
+        return False
+    outfile.write("\n");
+    return True
+
+def copy_orders(message, filename, sender):
+        # print the header first
+    if writeheaders:
+        from os.path import split
+        dirname, basename = split(filename)
+        dirname = dirname + '/headers'
+        if not os.path.exists(dirname): os.mkdir(dirname)
+        outfile = open(dirname + '/' + basename, "w")
+        for name, value in message.items():
+            outfile.write(name + ": " + value + "\n")
+        outfile.close()
+
+    found = False
+    outfile = open(filename, "w")
+    if message.is_multipart():
+        for part in message.get_payload():
+            content_type = part.get_content_type()
+            logger.debug("found content type %s for %s" % (content_type, sender))
+            if content_type=="text/plain":
+                if write_part(outfile, part):
+                    found = True
+                else:
+                    charset = part.get_content_charset()
+                    logger.error("could not write text/plain part (charset=%s) for %s" % (charset, sender))
+
+    else:
+        if write_part(outfile, message):
+            found = True
+        else:
+            charset = message.get_content_charset()
+            logger.error("could not write text/plain message (charset=%s) for %s" % (charset, sender))
+    outfile.close()
+    return found
+
+# create a file, containing:
+# game=0 locale=de file=/path/to/filename email=rcpt@domain.to
+def accept(game, locale, stream, extend=None):
+    global rootdir, orderbase, gamedir, gamename, sender
+    if extend is not None:
+        orderbase = orderbase + ".pre-" + extend
+    savedir = os.path.join(gamedir, orderbase)
+    # check if it's one of the pre-sent orders.
+    # create the save-directories if they don't exist
+    if not os.path.exists(gamedir): os.mkdir(gamedir)
+    if not os.path.exists(savedir): os.mkdir(savedir)
+    # parse message
+    message = Parser().parse(stream)
+    email = get_sender(message)
+    logger = logging.getLogger(email)
+    # write syslog
+    if email is None or valid_email(email)==0:
+        logger.warning("invalid email address: " + str(email))
+        return -1
+    logger.info("received orders from " + email)
+    # get an available filename
+    lock_file(gamedir + "/orders.queue")
+    maxdate, filename = available_file(savedir, prefix + email)
+    if filename is None:
+        logger.warning("more than " + str(maxfiles) + " orders from " + email)
+        return -1
+    # copy the orders to the file
+    text_ok = copy_orders(message, filename, email)
+    unlock_file(gamedir + "/orders.queue")
+
+    warning, msg, fail = None, "", False
+    maildate = message.get("Date")
+    if maildate != None:
+        turndate = mktime_tz(parsedate_tz(maildate))
+        os.utime(filename, (turndate, turndate))
+        logger.debug("mail date is '%s' (%d)" % (maildate, turndate))
+        if turndate < maxdate:
+            logger.warning("inconsistent message date " + email)
+            warning = " (" + messages["warning-" + locale] + ")"
+            msg = msg + formatpar(messages["maildate-" + locale] % (ctime(maxdate),ctime(turndate)), 76, 2) + "\n"
+    else:
+        logger.warning("missing message date " + email)
+        warning = " (" + messages["warning-" + locale] + ")"
+        msg = msg + formatpar(messages["nodate-" + locale], 76, 2) + "\n"
+
+    if not text_ok:
+        warning = " (" + messages["error-" + locale] + ")"
+        msg = msg + formatpar(messages["multipart-" + locale], 76, 2) + "\n"
+        logger.warning("rejected - no text/plain in orders from " + email)
+        os.unlink(filename)
+        savedir = savedir + "/rejected"
+        if not os.path.exists(savedir): os.mkdir(savedir)
+        lock_file(gamedir + "/orders.queue")
+        maxdate, filename = available_file(savedir, prefix + email)
+        store_message(message, filename)
+        unlock_file(gamedir + "/orders.queue")
+        fail = True
+
+    if sendmail and warning is not None:
+        subject = gamename + " " + messages["subject-"+locale] + warning
+        mail = "Subject: %s\nFrom: %s\nTo: %s\n\n" % (subject, sender, email) + msg
+        from smtplib import SMTP
+        server = SMTP("localhost")
+        server.sendmail(sender, email, mail)
+        server.close()
+
+    if not sendmail:
+        print text_ok, fail, email
+        print filename
+
+    if not fail:
+        lock_file(gamedir + "/orders.queue")
+        queue = open(gamedir + "/orders.queue", "a")
+        queue.write("email=%s file=%s locale=%s game=%s\n" % (email, filename, locale, game))
+        queue.close()
+        unlock_file(gamedir + "/orders.queue")
+
+    logger.info("done - accepted orders from " + email)
+
+    return 0
+
+# the main body of the script:
+try:
+    os.mkdir(os.path.join(rootdir, 'log'))
+except:
+    pass # already exists?
+LOG_FILENAME=os.path.join(rootdir, 'log/orders.log')
+logging.basicConfig(level=logging.DEBUG, filename=LOG_FILENAME)
+logger = logging
+delay=None # TODO: parse the turn delay
+locale = sys.argv[2]
+infile = stdin
+if len(sys.argv)>3:
+    infile = open(sys.argv[3], "r")
+retval = accept(game, locale, infile, delay)
+if infile!=stdin:
+    infile.close()
+sys.exit(retval)

From 5939b56b327451b957d7109227c5080335280fc5 Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno@kn-bremen.de>
Date: Sun, 3 Jun 2018 21:23:13 +0200
Subject: [PATCH 08/62] convert orders-process to use mutt update install
 process.

---
 .editorconfig             |   2 +-
 process/CMakeLists.txt    |   1 +
 process/orders-accept.py  | 378 --------------------------------------
 process/orders-process    | 243 +-----------------------
 process/process-orders.py | 215 ++++++++++++++++++++++
 5 files changed, 219 insertions(+), 620 deletions(-)
 delete mode 100755 process/orders-accept.py
 create mode 100755 process/process-orders.py

diff --git a/.editorconfig b/.editorconfig
index 9cebe6d6d..cd3315bd8 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -9,7 +9,7 @@ end_of_line = lf
 insert_final_newline = true
 
 # 4 space indentation
-[*.{c,h,lua}]
+[*.{c,h,lua,py}]
 indent_style = space
 indent_size = 4
 
diff --git a/process/CMakeLists.txt b/process/CMakeLists.txt
index 174fae694..0ffb7b4a2 100644
--- a/process/CMakeLists.txt
+++ b/process/CMakeLists.txt
@@ -1,5 +1,6 @@
 install(PROGRAMS create-orders backup-eressea run-turn send-zip-report
 send-bz2-report compress.py compress.sh epasswd.py orders-process
+process-orders.py accept-orders.py
 checkpasswd.py sendreport.sh sendreports.sh orders-accept DESTINATION bin)
 
 install(DIRECTORY cron/ DESTINATION bin USE_SOURCE_PERMISSIONS
diff --git a/process/orders-accept.py b/process/orders-accept.py
deleted file mode 100755
index 2f8f0bd29..000000000
--- a/process/orders-accept.py
+++ /dev/null
@@ -1,378 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: iso-8859-1 -*-
-
-from email.Utils import parseaddr
-from email.Parser import Parser
-import os
-import os.path
-import ConfigParser
-from re import compile, IGNORECASE
-from stat import ST_MTIME
-from string import upper, split, replace
-import logging
-import sys
-from sys import stdin
-from time import ctime, sleep, time
-from socket import gethostname
-from rfc822 import parsedate_tz, mktime_tz
-
-if 'ERESSEA' in os.environ:
-    dir = os.environ['ERESSEA']
-elif 'HOME' in os.environ:
-    dir = os.path.join(os.environ['HOME'], '/eressea')
-else: # WTF? No HOME?
-    dir = "/home/eressea/eressea"
-if not os.path.isdir(dir):
-    print "please set the ERESSEA environment variable to the install path"
-    sys.exit(1)
-rootdir = dir
-
-game = int(sys.argv[1])
-gamedir = os.path.join(rootdir, "game-%d" % (game, ))
-frommail = 'eressea-server@kn-bremen.de'
-gamename = 'Eressea'
-sender = '%s Server <%s>' % (gamename, frommail)
-
-inifile = os.path.join(gamedir, 'eressea.ini')
-if not os.path.exists(inifile):
-    print "no such file: " . inifile
-else:
-    config = ConfigParser.ConfigParser()
-    config.read(inifile)
-    if config.has_option('game', 'email'):
-        frommail = config.get('game', 'email')
-    if config.has_option('game', 'name'):
-        gamename = config.get('game', 'name')
-    if config.has_option('game', 'sender'):
-        sender = config.get('game', 'sender')
-    else:
-        sender = "%s Server <%s>" % (gamename, frommail)
-    config = None
-prefix = 'turn-'
-hostname = gethostname()
-orderbase = "orders.dir"
-sendmail = True
-# maximum number of reports per sender:
-maxfiles = 20
-# write headers to file?
-writeheaders = True
-# reject all html email?
-rejecthtml = True
-
-def unlock_file(filename):
-    try:
-        os.unlink(filename+".lock")
-    except:
-        print "could not unlock %s.lock, file not found" % filename
-
-def lock_file(filename):
-    i = 0
-    wait = 1
-    if not os.path.exists(filename):
-        file=open(filename, "w")
-        file.close()
-    while True:
-        try:
-            os.symlink(filename, filename+".lock")
-            return
-        except:
-            i = i+1
-            if i == 5: unlock_file(filename)
-            sleep(wait)
-            wait = wait*2
-
-messages = {
-        "multipart-en" :
-                "ERROR: The orders you sent contain no plaintext. " \
-                "The Eressea server cannot process orders containing HTML " \
-                "or invalid attachments, which are the reasons why this " \
-                "usually happens. Please change the settings of your mail " \
-                "software and re-send the orders.",
-
-        "multipart-de" :
-                "FEHLER: Die von dir eingeschickte Mail enth�lt keinen " \
-                "Text. Evtl. hast Du den Zug als HTML oder als anderweitig " \
-                "ung�ltig formatierte Mail ingeschickt. Wir k�nnen ihn " \
-                "deshalb nicht ber�cksichtigen. Schicke den Zug nochmals " \
-                "als reinen Text ohne Formatierungen ein.",
-
-        "maildate-de":
-                "Es erreichte uns bereits ein Zug mit einem sp�teren " \
-                "Absendedatum (%s > %s). Entweder ist deine " \
-                "Systemzeit verstellt, oder ein Zug hat einen anderen Zug von " \
-                "dir auf dem Transportweg �berholt. Entscheidend f�r die " \
-                "Auswertungsreihenfolge ist das Absendedatum, d.h. der Date:-Header " \
-                "deiner Mail.",
-
-        "maildate-en":
-                "The server already received an order file that was sent at a later " \
-                "date (%s > %s). Either your system clock is wrong, or two messages have " \
-                "overtaken each other on the way to the server. The order of " \
-                "execution on the server is always according to the Date: header in " \
-                "your mail.",
-
-        "nodate-en":
-                "Your message did not contain a valid Date: header in accordance with RFC2822.",
-
-        "nodate-de":
-                "Deine Nachricht enthielt keinen gueltigen Date: header nach RFC2822.",
-
-        "error-de":
-                "Fehler",
-
-        "error-en":
-                "Error",
-
-        "warning-de":
-                "Warnung",
-
-        "warning-en":
-                "Warning",
-
-        "subject-de":
-                "Befehle angekommen",
-
-        "subject-en":
-                "orders received"
-}
-
-# return 1 if addr is a valid email address
-def valid_email(addr):
-    rfc822_specials = '/()<>@,;:\\"[]'
-    # First we validate the name portion (name@domain)
-    c = 0
-    while c < len(addr):
-        if addr[c] == '"' and (not c or addr[c - 1] == '.' or addr[c - 1] == '"'):
-            c = c + 1
-            while c < len(addr):
-                if addr[c] == '"': break
-                if addr[c] == '\\' and addr[c + 1] == ' ':
-                    c = c + 2
-                    continue
-                if ord(addr[c]) < 32 or ord(addr[c]) >= 127: return 0
-                c = c + 1
-            else: return 0
-            if addr[c] == '@': break
-            if addr[c] != '.': return 0
-            c = c + 1
-            continue
-        if addr[c] == '@': break
-        if ord(addr[c]) <= 32 or ord(addr[c]) >= 127: return 0
-        if addr[c] in rfc822_specials: return 0
-        c = c + 1
-    if not c or addr[c - 1] == '.': return 0
-
-    # Next we validate the domain portion (name@domain)
-    domain = c = c + 1
-    if domain >= len(addr): return 0
-    count = 0
-    while c < len(addr):
-        if addr[c] == '.':
-            if c == domain or addr[c - 1] == '.': return 0
-            count = count + 1
-        if ord(addr[c]) <= 32 or ord(addr[c]) >= 127: return 0
-        if addr[c] in rfc822_specials: return 0
-        c = c + 1
-    return count >= 1
-
-# return the replyto or from address in the header
-def get_sender(header):
-    replyto = header.get("Reply-To")
-    if replyto is None:
-        replyto = header.get("From")
-        if replyto is None: return None
-    x = parseaddr(replyto)
-    return x[1]
-
-# return first available filename basename,[0-9]+
-def available_file(dirname, basename):
-    ver = 0
-    maxdate = 0
-    filename = "%s/%s,%s,%d" % (dirname, basename, hostname, ver)
-    while os.path.exists(filename):
-        maxdate = max(os.stat(filename)[ST_MTIME], maxdate)
-        ver = ver + 1
-        filename = "%s/%s,%s,%d" % (dirname, basename, hostname, ver)
-    if ver >= maxfiles:
-        return None, None
-    return maxdate, filename
-
-def formatpar(string, l=76, indent=2):
-    words = split(string)
-    res = ""
-    ll = 0
-    first = 1
-
-    for word in words:
-        if first == 1:
-            res = word
-            first = 0
-            ll = len(word)
-        else:
-            if ll + len(word) > l:
-                res = res + "\n"+" "*indent+word
-                ll = len(word) + indent
-            else:
-                res = res+" "+word
-                ll = ll + len(word) + 1
-
-    return res+"\n"
-
-def store_message(message, filename):
-    outfile = open(filename, "w")
-    outfile.write(message.as_string())
-    outfile.close()
-    return
-
-def write_part(outfile, part):
-    charset = part.get_content_charset()
-    payload = part.get_payload(decode=True)
-
-    if charset is None:
-        charset = "latin1"
-    try:
-        msg = payload.decode(charset, "ignore")
-    except:
-        msg = payload
-        charset = None
-    try:
-        utf8 = msg.encode("utf-8", "ignore")
-        outfile.write(utf8)
-    except:
-        outfile.write(msg)
-        return False
-    outfile.write("\n");
-    return True
-
-def copy_orders(message, filename, sender):
-        # print the header first
-    if writeheaders:
-        from os.path import split
-        dirname, basename = split(filename)
-        dirname = dirname + '/headers'
-        if not os.path.exists(dirname): os.mkdir(dirname)
-        outfile = open(dirname + '/' + basename, "w")
-        for name, value in message.items():
-            outfile.write(name + ": " + value + "\n")
-        outfile.close()
-
-    found = False
-    outfile = open(filename, "w")
-    if message.is_multipart():
-        for part in message.get_payload():
-            content_type = part.get_content_type()
-            logger.debug("found content type %s for %s" % (content_type, sender))
-            if content_type=="text/plain":
-                if write_part(outfile, part):
-                    found = True
-                else:
-                    charset = part.get_content_charset()
-                    logger.error("could not write text/plain part (charset=%s) for %s" % (charset, sender))
-
-    else:
-        if write_part(outfile, message):
-            found = True
-        else:
-            charset = message.get_content_charset()
-            logger.error("could not write text/plain message (charset=%s) for %s" % (charset, sender))
-    outfile.close()
-    return found
-
-# create a file, containing:
-# game=0 locale=de file=/path/to/filename email=rcpt@domain.to
-def accept(game, locale, stream, extend=None):
-    global rootdir, orderbase, gamedir, gamename, sender
-    if extend is not None:
-        orderbase = orderbase + ".pre-" + extend
-    savedir = os.path.join(gamedir, orderbase)
-    # check if it's one of the pre-sent orders.
-    # create the save-directories if they don't exist
-    if not os.path.exists(gamedir): os.mkdir(gamedir)
-    if not os.path.exists(savedir): os.mkdir(savedir)
-    # parse message
-    message = Parser().parse(stream)
-    email = get_sender(message)
-    logger = logging.getLogger(email)
-    # write syslog
-    if email is None or valid_email(email)==0:
-        logger.warning("invalid email address: " + str(email))
-        return -1
-    logger.info("received orders from " + email)
-    # get an available filename
-    lock_file(gamedir + "/orders.queue")
-    maxdate, filename = available_file(savedir, prefix + email)
-    if filename is None:
-        logger.warning("more than " + str(maxfiles) + " orders from " + email)
-        return -1
-    # copy the orders to the file
-    text_ok = copy_orders(message, filename, email)
-    unlock_file(gamedir + "/orders.queue")
-
-    warning, msg, fail = None, "", False
-    maildate = message.get("Date")
-    if maildate != None:
-        turndate = mktime_tz(parsedate_tz(maildate))
-        os.utime(filename, (turndate, turndate))
-        logger.debug("mail date is '%s' (%d)" % (maildate, turndate))
-        if turndate < maxdate:
-            logger.warning("inconsistent message date " + email)
-            warning = " (" + messages["warning-" + locale] + ")"
-            msg = msg + formatpar(messages["maildate-" + locale] % (ctime(maxdate),ctime(turndate)), 76, 2) + "\n"
-    else:
-        logger.warning("missing message date " + email)
-        warning = " (" + messages["warning-" + locale] + ")"
-        msg = msg + formatpar(messages["nodate-" + locale], 76, 2) + "\n"
-
-    if not text_ok:
-        warning = " (" + messages["error-" + locale] + ")"
-        msg = msg + formatpar(messages["multipart-" + locale], 76, 2) + "\n"
-        logger.warning("rejected - no text/plain in orders from " + email)
-        os.unlink(filename)
-        savedir = savedir + "/rejected"
-        if not os.path.exists(savedir): os.mkdir(savedir)
-        lock_file(gamedir + "/orders.queue")
-        maxdate, filename = available_file(savedir, prefix + email)
-        store_message(message, filename)
-        unlock_file(gamedir + "/orders.queue")
-        fail = True
-
-    if sendmail and warning is not None:
-        subject = gamename + " " + messages["subject-"+locale] + warning
-        mail = "Subject: %s\nFrom: %s\nTo: %s\n\n" % (subject, sender, email) + msg
-        from smtplib import SMTP
-        server = SMTP("localhost")
-        server.sendmail(sender, email, mail)
-        server.close()
-
-    if not sendmail:
-        print text_ok, fail, email
-        print filename
-
-    if not fail:
-        lock_file(gamedir + "/orders.queue")
-        queue = open(gamedir + "/orders.queue", "a")
-        queue.write("email=%s file=%s locale=%s game=%s\n" % (email, filename, locale, game))
-        queue.close()
-        unlock_file(gamedir + "/orders.queue")
-
-    logger.info("done - accepted orders from " + email)
-
-    return 0
-
-# the main body of the script:
-try:
-    os.mkdir(os.path.join(rootdir, 'log'))
-except:
-    pass # already exists?
-LOG_FILENAME=os.path.join(rootdir, 'log/orders.log')
-logging.basicConfig(level=logging.DEBUG, filename=LOG_FILENAME)
-logger = logging
-delay=None # TODO: parse the turn delay
-locale = sys.argv[2]
-infile = stdin
-if len(sys.argv)>3:
-    infile = open(sys.argv[3], "r")
-retval = accept(game, locale, infile, delay)
-if infile!=stdin:
-    infile.close()
-sys.exit(retval)
diff --git a/process/orders-process b/process/orders-process
index 79c7b3378..09ca7dc0c 100755
--- a/process/orders-process
+++ b/process/orders-process
@@ -1,242 +1,3 @@
-#!/usr/bin/env python
-# -*- coding: iso-8859-1 -*-
+#!/bin/sh
+python process-orders.py "$@"
 
-from os import unlink, symlink, rename, popen, tmpfile
-import sys
-import os
-import os.path
-import ConfigParser
-from re import compile, IGNORECASE
-from string import split, join, upper, strip
-from sys import argv, exit
-from time import sleep, time, ctime
-from syslog import openlog, closelog, syslog
-
-from epasswd import EPasswd
-
-def pwd_get_email(faction, pwd, pwdfile=None):
-    return None
-
-def split_filename(filename):
-    return os.path.split(filename)
-
-def unlock_file(filename):
-    try:
-        unlink(filename+".lock")
-    except:
-        print "could not unlock %s.lock, file not found" % filename
-        raise
-    
-def lock_file(filename):
-    i = 0
-    wait = 1
-    if not os.path.exists(filename):
-        file=open(filename, "w")
-        file.close()
-    while True:
-        try:
-            symlink(filename, filename+".lock")
-            return
-        except:
-            i = i+1
-            if i == 5:
-                raise
-            sleep(wait)
-            wait = wait*2
-
-messages = {
-"subject-de": "Befehle angekommen",
-"subject-en": "orders received",
-
-"validate-en": "Validating",
-"validate-de": "Verarbeite",
-
-"faction-en": "Faction",
-"faction-de": "Partei",
-
-"unknown-de": "WARNUNG: Die Partei ist nicht bekannt, oder das Passwort falsch!",
-"unknown-en": "WARNING: This faction is unknown, or the password is incorrect!",
-
-"warning-de": "Warnung",
-"warning-en": "Warning",
-
-"error-de": "Fehler",
-"error-en": "Error",
-}
-
-game = int(sys.argv[1])
-echeck_cmd = "/home/eressea/echeck/echeck.sh"
-maxlines = 25
-
-# base directory for all your games:
-install_dir = "/home/eressea/eressea"
-if 'ERESSEA' in os.environ:
-    install_dir = os.environ['ERESSEA']
-elif 'HOME' in os.environ:
-    install_dir = os.path.join(os.environ['HOME'], '/eressea')
-if not os.path.isdir(install_dir):
-    print "please set the ERESSEA environment variable to the install path"
-    sys.exit(1)
-
-game_dir = os.path.join(install_dir, "game-%d" % (game, ))
-frommail = 'eressea-server@kn-bremen.de'
-gamename = 'Eressea'
-sender = '%s Server <%s>' % (gamename, frommail)
-
-inifile = os.path.join(game_dir, 'eressea.ini')
-if not os.path.exists(inifile):
-    print "no such file: " . inifile
-else:
-    config = ConfigParser.ConfigParser()
-    config.read(inifile)
-    if config.has_option('game', 'email'):
-        frommail = config.get('game', 'email')
-    if config.has_option('game', 'name'):
-        gamename = config.get('game', 'name')
-    if config.has_option('game', 'sender'):
-        sender = config.get('game', 'sender')
-    else:
-        sender = "%s Server <%s>" % (gamename, frommail)
-    config = None
-
-queue_file = os.path.join(game_dir, "orders.queue")
-if not os.path.exists(queue_file):
-    exit(0)
-
-# regular expression that finds the start of a faction
-fact_re = compile("^\s*(eressea|partei|faction)\s+([a-zA-Z0-9]+)\s+\"?([^\"]*)\"?", IGNORECASE)
-
-def check_pwd(filename, email, pw_data):
-    results = []
-    try:
-        file = open(filename, "r")
-    except:
-        print "could not open file", filename
-        return results
-    for line in file.readlines():
-        mo = fact_re.search(strip(line))
-        if mo != None:
-            fact_nr = str(mo.group(2))
-            fact_pw = str(mo.group(3))
-            if pw_data.fac_exists(fact_nr):
-                if not pw_data.check(fact_nr, fact_pw):
-                    game_email = pw_data.get_email(fact_nr)
-                    results = results + [ (fact_nr, game_email, False, fact_pw) ]
-                else:
-                    game_email = pw_data.get_email(fact_nr)
-                    results = results + [ (fact_nr, game_email, True, fact_pw) ]
-            else:
-                results = results + [ (fact_nr, None, False, fact_pw) ]
-    return results
-
-def echeck(filename, locale, rules):
-    dirname, filename = split_filename(filename)
-    stream = popen("%s %s %s %s %s" % (echeck_cmd, locale, filename, dirname, rules), 'r')
-    lines = stream.readlines()
-    if len(lines)==0:
-        stream.close()
-        return None
-    if len(lines)>maxlines:
-        mail = join(lines[:maxlines-3] + ["...", "\n"] + lines[-3:], '')
-    else:
-        mail = join(lines[:maxlines], '')
-    stream.close()
-    return mail
-
-# parse the queue file -
-#print "connecting to SMTP..."
-from smtplib import SMTP
-try:
-    server = SMTP("localhost")
-except:
-    print "could not connect to SMTP server"
-    exit(0)
-
-#print "reading password file..."
-pw_data = EPasswd(os.path.join(game_dir,"passwd"))
-
-#print "reading orders.queue..."
-# move the queue file to a save space while locking it:
-try:
-    lock_file(queue_file)
-except:
-    exit(0)
-queuefile = open(queue_file, "r")
-lines = queuefile.readlines()
-queuefile.close()
-
-# copy to a temp file
-
-tname="/tmp/orders.queue.%s" % str(time())
-try:
-    lock_file(tname)
-except:
-    exit(0)
-tmpfile=open(tname, "w")
-for line in lines:
-    tmpfile.write(line)
-tmpfile.close()
-
-openlog("orders")
-
-unlink(queue_file)
-try:
-    unlock_file(queue_file)
-except:
-    pass
-
-for line in lines:
-    tokens = split(line[:-1], ' ')
-    dict = {}
-    for token in tokens:
-        name, value = split(token, '=')
-        dict[name] = value
-
-    email = dict["email"]
-    locale = dict["locale"]
-    game = int(dict["game"])
-    infile = dict["file"]
-    gamename='[E%d]' % game
-    rules='e%d' % game
-    warning = ""
-    failed = True
-    results = check_pwd(infile, email, pw_data)
-    logfile = open(os.path.join(game_dir, "zug.log"), "a")
-    dirname, filename = split_filename(infile)
-    msg = messages["validate-"+locale] + " " + infile + "\n\n"
-    for faction, game_email, success, pwd in results:
-        msg = msg + messages["faction-"+locale] + " " + faction + "\n"
-        if success: failed = False
-        else: msg = msg + messages["unknown-"+locale] + "\n"
-        msg = msg + "\n"
-        logfile.write("%s:%s:%s:%s:%s:%s\n" % (ctime(time()), email, game_email, faction, pwd, success))
-    logfile.close()
-
-    if failed:
-        warning = " (" + messages["warning-" + locale] + ")"
-        syslog("failed - no valid password in " + infile)
-    else:
-        result = None
-        if os.path.exists(echeck_cmd):
-            result = echeck(infile, locale, rules)
-        if result is None:
-            # echeck did not finish
-            msg = msg + "Echeck is broken. Your turn was accepted, but could not be verified.\n"
-            warning = " (" + messages["warning-" + locale] + ")"
-            syslog("process - echeck broken, " + infile)
-        else:
-            msg = msg + result
-            syslog("process - checked orders in " + infile)
-
-    subject = gamename + " " + messages["subject-" + locale] + warning
-    msg = "Subject: %s\nFrom: %s\nTo: %s\nContent-Type: text/plain; charset=utf-8\n\n" % (subject, sender, email) + msg
-    try:
-        server.sendmail(sender, email, msg)
-    except:
-        syslog("failed - cannot send to " + email)
-
-server.close()
-
-closelog()
-unlink(tname)
-unlock_file(tname)
diff --git a/process/process-orders.py b/process/process-orders.py
new file mode 100755
index 000000000..8ed5e8625
--- /dev/null
+++ b/process/process-orders.py
@@ -0,0 +1,215 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from os import unlink, symlink, rename, popen, tmpfile
+import sys
+import os
+import os.path
+import ConfigParser
+import subprocess
+from re import compile, IGNORECASE
+from string import split, join, upper, strip
+from sys import argv, exit
+from time import sleep, time, ctime
+from syslog import openlog, closelog, syslog
+
+from epasswd import EPasswd
+
+def pwd_get_email(faction, pwd, pwdfile=None):
+    return None
+
+def split_filename(filename):
+    return os.path.split(filename)
+
+def unlock_file(filename):
+    try:
+        unlink(filename+".lock")
+    except:
+        print "could not unlock %s.lock, file not found" % filename
+        raise
+    
+def lock_file(filename):
+    i = 0
+    wait = 1
+    if not os.path.exists(filename):
+        file=open(filename, "w")
+        file.close()
+    while True:
+        try:
+            symlink(filename, filename+".lock")
+            return
+        except:
+            i = i+1
+            if i == 5:
+                raise
+            sleep(wait)
+            wait = wait*2
+
+messages = {
+"subject-de": "Befehle angekommen",
+"subject-en": "orders received",
+
+"validate-en": "Validating",
+"validate-de": "Verarbeite",
+
+"faction-en": "Faction",
+"faction-de": "Partei",
+
+"unknown-de": "WARNUNG: Die Partei ist nicht bekannt, oder das Passwort falsch!",
+"unknown-en": "WARNING: This faction is unknown, or the password is incorrect!",
+
+"warning-de": "Warnung",
+"warning-en": "Warning",
+
+"error-de": "Fehler",
+"error-en": "Error",
+}
+
+game = int(sys.argv[1])
+echeck_cmd = "/home/eressea/echeck/echeck.sh"
+maxlines = 25
+
+# base directory for all your games:
+install_dir = "/home/eressea/eressea"
+if 'ERESSEA' in os.environ:
+    install_dir = os.environ['ERESSEA']
+elif 'HOME' in os.environ:
+    install_dir = os.path.join(os.environ['HOME'], 'eressea')
+if not os.path.isdir(install_dir):
+    print "please set the ERESSEA environment variable to the install path"
+    sys.exit(1)
+
+game_dir = os.path.join(install_dir, "game-%d" % (game, ))
+gamename = 'Eressea'
+
+inifile = os.path.join(game_dir, 'eressea.ini')
+queue_file = os.path.join(game_dir, "orders.queue")
+if not os.path.exists(queue_file):
+    exit(0)
+
+# regular expression that finds the start of a faction
+fact_re = compile("^\s*(eressea|partei|faction)\s+([a-zA-Z0-9]+)\s+\"?([^\"]*)\"?", IGNORECASE)
+
+def check_pwd(filename, email, pw_data):
+    results = []
+    try:
+        file = open(filename, "r")
+    except:
+        print "could not open file", filename
+        return results
+    for line in file.readlines():
+        mo = fact_re.search(strip(line))
+        if mo != None:
+            fact_nr = str(mo.group(2))
+            fact_pw = str(mo.group(3))
+            if pw_data.fac_exists(fact_nr):
+                if not pw_data.check(fact_nr, fact_pw):
+                    game_email = pw_data.get_email(fact_nr)
+                    results = results + [ (fact_nr, game_email, False, fact_pw) ]
+                else:
+                    game_email = pw_data.get_email(fact_nr)
+                    results = results + [ (fact_nr, game_email, True, fact_pw) ]
+            else:
+                results = results + [ (fact_nr, None, False, fact_pw) ]
+    return results
+
+def echeck(filename, locale, rules):
+    dirname, filename = split_filename(filename)
+    stream = popen("%s %s %s %s %s" % (echeck_cmd, locale, filename, dirname, rules), 'r')
+    lines = stream.readlines()
+    if len(lines)==0:
+        stream.close()
+        return None
+    if len(lines)>maxlines:
+        mail = join(lines[:maxlines-3] + ["...", "\n"] + lines[-3:], '')
+    else:
+        mail = join(lines[:maxlines], '')
+    stream.close()
+    return mail
+
+#print "reading password file..."
+pw_data = EPasswd(os.path.join(game_dir,"passwd"))
+
+#print "reading orders.queue..."
+# move the queue file to a save space while locking it:
+try:
+    lock_file(queue_file)
+except:
+    exit(0)
+queuefile = open(queue_file, "r")
+lines = queuefile.readlines()
+queuefile.close()
+
+# copy to a temp file
+
+tname="/tmp/orders.queue.%s" % str(time())
+try:
+    lock_file(tname)
+except:
+    exit(0)
+tmpfile=open(tname, "w")
+for line in lines:
+    tmpfile.write(line)
+tmpfile.close()
+
+openlog("orders")
+
+unlink(queue_file)
+try:
+    unlock_file(queue_file)
+except:
+    pass
+
+for line in lines:
+    tokens = split(line[:-1], ' ')
+    dict = {}
+    for token in tokens:
+        name, value = split(token, '=')
+        dict[name] = value
+
+    email = dict["email"]
+    locale = dict["locale"]
+    game = int(dict["game"])
+    infile = dict["file"]
+    gamename='[E%d]' % game
+    rules='e%d' % game
+    warning = ""
+    failed = True
+    results = check_pwd(infile, email, pw_data)
+    logfile = open(os.path.join(game_dir, "zug.log"), "a")
+    dirname, filename = split_filename(infile)
+    msg = messages["validate-"+locale] + " " + infile + "\n\n"
+    for faction, game_email, success, pwd in results:
+        msg = msg + messages["faction-"+locale] + " " + faction + "\n"
+        if success: failed = False
+        else: msg = msg + messages["unknown-"+locale] + "\n"
+        msg = msg + "\n"
+        logfile.write("%s:%s:%s:%s:%s:%s\n" % (ctime(time()), email, game_email, faction, pwd, success))
+    logfile.close()
+
+    if failed:
+        warning = " (" + messages["warning-" + locale] + ")"
+        syslog("failed - no valid password in " + infile)
+    else:
+        result = None
+        if os.path.exists(echeck_cmd):
+            result = echeck(infile, locale, rules)
+        if result is None:
+            # echeck did not finish
+            msg = msg + "Echeck is broken. Your turn was accepted, but could not be verified.\n"
+            warning = " (" + messages["warning-" + locale] + ")"
+            syslog("process - echeck broken, " + infile)
+        else:
+            msg = msg + result
+            syslog("process - checked orders in " + infile)
+
+    subject = gamename + " " + messages["subject-" + locale] + warning
+    try:
+        sp = subprocess.Popen(['mutt', '-s', subject, email], stdin=subprocess.PIPE)
+        sp.communicate(msg)
+    except:
+        syslog("failed - cannot send to " + email)
+
+closelog()
+unlink(tname)
+unlock_file(tname)

From 1c6f598fddbede5590ea70acc6b0567fd27d1622 Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno@kn-bremen.de>
Date: Tue, 5 Jun 2018 21:14:02 +0200
Subject: [PATCH 09/62] fix orders script running from install directory

---
 process/orders-accept  | 2 ++
 process/orders-process | 2 ++
 2 files changed, 4 insertions(+)

diff --git a/process/orders-accept b/process/orders-accept
index bd8ae523f..266424a41 100755
--- a/process/orders-accept
+++ b/process/orders-accept
@@ -1,3 +1,5 @@
 #!/bin/sh
+SCRIPT=$(readlink -f $0)
+cd $(dirname $SCRIPT)
 python accept-orders.py "$@"
 
diff --git a/process/orders-process b/process/orders-process
index 09ca7dc0c..38b2d1115 100755
--- a/process/orders-process
+++ b/process/orders-process
@@ -1,3 +1,5 @@
 #!/bin/sh
+SCRIPT=$(readlink -f $0)
+cd $(dirname $SCRIPT)
 python process-orders.py "$@"
 

From 13090fd27c314a748dea5adf456c512880b84362 Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Sun, 10 Jun 2018 09:51:08 +0200
Subject: [PATCH 10/62] BUG 2446: trade prices not getting read from XML.

---
 scripts/tests/e2/e2features.lua | 23 ++++++++++++++++++++++-
 src/exparse.c                   |  4 +++-
 2 files changed, 25 insertions(+), 2 deletions(-)

diff --git a/scripts/tests/e2/e2features.lua b/scripts/tests/e2/e2features.lua
index 4758fce39..be09ec448 100644
--- a/scripts/tests/e2/e2features.lua
+++ b/scripts/tests/e2/e2features.lua
@@ -491,8 +491,29 @@ function test_dwarf_mining()
     local f = faction.create('dwarf')
     local r = region.create(0, 0, 'plain')
     local u = unit.create(f, r)
-    u.name = 'Xolgrim'
     u:set_skill('mining', 2)
     assert_equal(2, u:get_skill('mining'))
     assert_equal(4, u:eff_skill('mining'))
 end
+
+function test_buy_sell()
+    local f = faction.create('human')
+    local r = region.create(0, 0, 'plain')
+    local u = unit.create(f, r)
+    local lux = r.luxury
+    local b = building.create(r, 'castle')
+    b.size = 10
+    u:set_skill('trade', 1)
+    item = 'silk'
+    name = 'Seide'
+    if lux == 'silk' then
+        item = 'balm'
+        name = 'Balsam'
+    end
+    u:add_item(item, 5)
+    u:add_order('VERKAUFE 1 ' .. name)
+    assert_equal(0, u:get_item('money'))
+    process_orders()
+    assert_equal(4, u:get_item(item))
+    assert_not_equal(0, u:get_item('money'))
+end
diff --git a/src/exparse.c b/src/exparse.c
index e6975fc17..ed6452e40 100644
--- a/src/exparse.c
+++ b/src/exparse.c
@@ -818,7 +818,9 @@ static void start_resources(parseinfo *pi, const XML_Char *el, const XML_Char **
                 handle_requirement(pi, el, attr);
             }
             else if (xml_strcmp(el, "luxury") == 0) {
-                rtype->ltype = new_luxurytype(itype, 0);
+                int price = atoi(attr_get(attr, "price"));
+                assert(price > 0);
+                rtype->ltype = new_luxurytype(itype, price);
             }
             else if (xml_strcmp(el, "potion") == 0) {
                 int i, level = 0;

From cf76e0a03c7a4286bc897a748ad62ae816052616 Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Sun, 10 Jun 2018 10:16:34 +0200
Subject: [PATCH 11/62] BUG 2443, list of roads missing whitespace.

---
 res/translations/strings.de.po | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/res/translations/strings.de.po b/res/translations/strings.de.po
index e56e36f55..8197e7821 100644
--- a/res/translations/strings.de.po
+++ b/res/translations/strings.de.po
@@ -1907,7 +1907,7 @@ msgid "castle"
 msgstr "Burg"
 
 msgid "nr_borderlist_infix"
-msgstr ", im"
+msgstr ", im "
 
 msgctxt "race"
 msgid "shadowbat_p"
@@ -3707,7 +3707,7 @@ msgid "analyse_object"
 msgstr "Lied des Ortes analysieren"
 
 msgid "nr_borderlist_lastfix"
-msgstr "und im"
+msgstr "und im "
 
 msgctxt "race"
 msgid "shadowknight_d"

From 3c57917b7906b1174a5aef822228bd49f469b59d Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Sun, 10 Jun 2018 10:23:35 +0200
Subject: [PATCH 12/62] BUG 2443: spaces for list of roads, again!

---
 res/translations/strings.de.po | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/res/translations/strings.de.po b/res/translations/strings.de.po
index 8197e7821..95d973e35 100644
--- a/res/translations/strings.de.po
+++ b/res/translations/strings.de.po
@@ -5573,7 +5573,7 @@ msgid "swamp_trail"
 msgstr "der Sumpf von %s"
 
 msgid "nr_nb_final"
-msgstr "und im"
+msgstr " und im "
 
 msgid "aoc"
 msgstr "Katzenamulett"

From f7d4388232c18d707461641a3c250b55b792e5eb Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Sun, 10 Jun 2018 10:23:35 +0200
Subject: [PATCH 13/62] BUG 2443: spaces for list of roads, again!

---
 res/translations/strings.de.po | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/res/translations/strings.de.po b/res/translations/strings.de.po
index 95d973e35..622f4a536 100644
--- a/res/translations/strings.de.po
+++ b/res/translations/strings.de.po
@@ -5573,7 +5573,7 @@ msgid "swamp_trail"
 msgstr "der Sumpf von %s"
 
 msgid "nr_nb_final"
-msgstr " und im "
+msgstr "und im "
 
 msgid "aoc"
 msgstr "Katzenamulett"

From 2b0b7cd0f81c00fb83398fdac956c47149cac815 Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Mon, 18 Jun 2018 20:33:28 +0200
Subject: [PATCH 14/62] BUG 2447: PArtei fehlt im Kampfreport. replace strcpy
 with strcat

---
 src/reports.c | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/src/reports.c b/src/reports.c
index 06f6b0084..d240ee86d 100644
--- a/src/reports.c
+++ b/src/reports.c
@@ -2179,10 +2179,10 @@ void report_battle_start(battle * b)
             for (df = s->fighters; df; df = df->next) {
                 if (is_attacker(df)) {
                     if (first) {
-                        sbs_strcpy(&sbs, ", ");
+                        sbs_strcat(&sbs, ", ");
                     }
                     if (lastf) {
-                        sbs_strcpy(&sbs, lastf);
+                        sbs_strcat(&sbs, lastf);
                         first = true;
                     }
                     if (seematrix(f, s))
@@ -2193,12 +2193,10 @@ void report_battle_start(battle * b)
                 }
             }
         }
-        if (first) {
+        if (first && lastf) {
             sbs_strcat(&sbs, " ");
             sbs_strcat(&sbs, LOC(f->locale, "and"));
             sbs_strcat(&sbs, " ");
-        }
-        if (lastf) {
             sbs_strcat(&sbs, lastf);
         }
 

From 501830817051c581665ee5714bde5156d3c34712 Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Thu, 28 Jun 2018 22:48:57 +0200
Subject: [PATCH 15/62] remove at_chaoscount, update storage module.

---
 clibs                       |  2 +-
 src/attributes/attributes.c |  1 +
 src/battle.c                |  2 --
 src/bind_region.c           |  9 +-----
 src/chaos.c                 | 61 -------------------------------------
 src/chaos.h                 |  7 -----
 src/eressea.c               |  2 --
 src/gmtool.c                |  2 --
 src/kernel/order.test.c     |  1 +
 src/monsters.c              | 20 +++++-------
 storage                     |  2 +-
 11 files changed, 13 insertions(+), 96 deletions(-)

diff --git a/clibs b/clibs
index f9842e07a..bd70ef20b 160000
--- a/clibs
+++ b/clibs
@@ -1 +1 @@
-Subproject commit f9842e07a442c5453c270badf25ab72633b4edf5
+Subproject commit bd70ef20babbe9949e5ad5acd5317e7d39ad1e72
diff --git a/src/attributes/attributes.c b/src/attributes/attributes.c
index 907478aaa..fa6f15d73 100644
--- a/src/attributes/attributes.c
+++ b/src/attributes/attributes.c
@@ -209,6 +209,7 @@ void register_attributes(void)
 
     at_deprecate("maxmagicians", a_readint); /* factions with differnt magician limits, probably unused */
     at_deprecate("hurting", a_readint); /* an old arena attribute */
+    at_deprecate("chaoscount", a_readint); /* used to increase the chance of monster spawns */
     at_deprecate("xontormiaexpress", a_readint);    /* required for old datafiles */
     at_deprecate("orcification", a_readint);    /* required for old datafiles */
     at_deprecate("lua", read_ext);    /* required for old datafiles */
diff --git a/src/battle.c b/src/battle.c
index 9092718a2..be950af7f 100644
--- a/src/battle.c
+++ b/src/battle.c
@@ -20,7 +20,6 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 #include <kernel/config.h>
 #include "battle.h"
 #include "alchemy.h"
-#include "chaos.h"
 #include "guard.h"
 #include "laws.h"
 #include "monsters.h"
@@ -2531,7 +2530,6 @@ static void battle_effects(battle * b, int dead_players)
         }
         if (dead_peasants) {
             deathcounts(r, dead_peasants + dead_players);
-            add_chaoscount(r, dead_peasants / 2);
             rsetpeasants(r, rp - dead_peasants);
         }
     }
diff --git a/src/bind_region.c b/src/bind_region.c
index e041fba77..b49ceb078 100644
--- a/src/bind_region.c
+++ b/src/bind_region.c
@@ -7,7 +7,6 @@
 #include "bind_ship.h"
 #include "bind_building.h"
 
-#include "chaos.h"
 #include "teleport.h"
 
 #include <kernel/calendar.h>
@@ -356,7 +355,7 @@ static int tolua_region_get_resourcelevel(lua_State * L)
 #define LUA_ASSERT(c, s) if (!(c)) { log_error("%s(%d): %s\n", __FILE__, __LINE__, (s)); return 0; }
 
 static int special_resource(const char *type) {
-    const char * special[] = { "seed", "sapling", "tree", "grave", "chaos", 0 };
+    const char * special[] = { "seed", "sapling", "tree", "grave", NULL };
     int i;
 
     for (i = 0; special[i]; ++i) {
@@ -389,9 +388,6 @@ static int tolua_region_get_resource(lua_State * L)
     case 3:
         result = deathcount(r);
         break;
-    case 4:
-        result = get_chaoscount(r);
-        break;
     default:
         rtype = rt_find(type);
         if (rtype) {
@@ -423,9 +419,6 @@ static int tolua_region_set_resource(lua_State * L)
     case 3:
         deathcounts(r, value - deathcount(r));
         break;
-    case 4:
-        add_chaoscount(r, value - get_chaoscount(r));
-        break;
     default:
         rtype = rt_find(type);
         if (rtype != NULL) {
diff --git a/src/chaos.c b/src/chaos.c
index 67cdb356d..21a5a8624 100644
--- a/src/chaos.c
+++ b/src/chaos.c
@@ -39,57 +39,6 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 #include <stdlib.h>
 #include <assert.h>
 
-/*********************/
-/*   at_chaoscount   */
-/*********************/
-attrib_type at_chaoscount = {
-    "chaoscount",
-    DEFAULT_INIT,
-    DEFAULT_FINALIZE,
-    DEFAULT_AGE,
-    a_writeint,
-    a_readint,
-    NULL,
-    ATF_UNIQUE
-};
-
-void set_chaoscount(struct region *r, int deaths)
-{
-    if (deaths==0) {
-        a_removeall(&r->attribs, &at_chaoscount);
-    } else {
-        attrib *a = a_find(r->attribs, &at_chaoscount);
-        if (!a) {
-            a = a_add(&r->attribs, a_new(&at_chaoscount));
-        }
-        a->data.i = deaths;
-    }
-}
-
-int get_chaoscount(const region * r)
-{
-    attrib *a = a_find(r->attribs, &at_chaoscount);
-    if (!a)
-        return 0;
-    return a->data.i;
-}
-
-void add_chaoscount(region * r, int fallen)
-{
-    attrib *a;
-
-    if (fallen == 0)
-        return;
-
-    a = a_find(r->attribs, &at_chaoscount);
-    if (!a)
-        a = a_add(&r->attribs, a_new(&at_chaoscount));
-    a->data.i += fallen;
-
-    if (a->data.i <= 0)
-        a_remove(&r->attribs, a);
-}
-
 static const terrain_type *chaosterrain(void)
 {
     static const terrain_type **types;
@@ -248,18 +197,8 @@ void chaos_update(void) {
     region *r;
     /* Chaos */
     for (r = regions; r; r = r->next) {
-        int i;
-
         if ((r->flags & RF_CHAOTIC)) {
             chaos(r);
         }
-        i = get_chaoscount(r);
-        if (i) {
-            add_chaoscount(r, -(int)(i * ((double)(rng_int() % 10)) / 100.0));
-        }
     }
 }
-
-void chaos_register(void) {
-    at_register(&at_chaoscount);
-}
diff --git a/src/chaos.h b/src/chaos.h
index 0c2667083..550aa3efb 100644
--- a/src/chaos.h
+++ b/src/chaos.h
@@ -24,15 +24,8 @@ extern "C" {
 
     struct region;
 
-    extern struct attrib_type at_chaoscount;
-
-    void chaos_register(void);
     void chaos_update(void);
 
-    void set_chaoscount(struct region *r, int deaths);
-    int get_chaoscount(const struct region * r);
-    void add_chaoscount(struct region * r, int deaths);
-
 #ifdef __cplusplus
 }
 #endif
diff --git a/src/eressea.c b/src/eressea.c
index 982dfe6ee..0bf1454ee 100644
--- a/src/eressea.c
+++ b/src/eressea.c
@@ -24,7 +24,6 @@
 #include "attributes/attributes.h"
 #include "races/races.h"
 
-#include "chaos.h"
 #include "items.h"
 #include "creport.h"
 #include "report.h"
@@ -82,5 +81,4 @@ void game_init(void)
     register_attributes();
     register_gmcmd();
 
-    chaos_register();
 }
diff --git a/src/gmtool.c b/src/gmtool.c
index e13701f6f..d3858d09f 100644
--- a/src/gmtool.c
+++ b/src/gmtool.c
@@ -50,7 +50,6 @@
 #include <util/unicode.h>
 
 #include "gmtool_structs.h"
-#include "chaos.h"
 #include "console.h"
 #include "listbox.h"
 #include "wormhole.h"
@@ -531,7 +530,6 @@ static void statusline(WINDOW * win, const char *str)
 }
 
 static void reset_region(region *r) {
-    set_chaoscount(r, 0);
     r->flags = 0;
     a_removeall(&r->attribs, NULL);
     while (r->units) {
diff --git a/src/kernel/order.test.c b/src/kernel/order.test.c
index 37f88ae5a..3a2f44c56 100644
--- a/src/kernel/order.test.c
+++ b/src/kernel/order.test.c
@@ -512,6 +512,7 @@ static void test_create_order_long(CuTest *tc) {
     stream_order(&out, ord, lang, true);
     out.api->rewind(out.handle);
     out.api->readln(out.handle, buffer, sizeof(buffer));
+    CuAssertIntEquals(tc, 1026, strlen(buffer));
     mstream_done(&out);
     free_order(ord);
     test_teardown();
diff --git a/src/monsters.c b/src/monsters.c
index be4ec7525..181615a40 100644
--- a/src/monsters.c
+++ b/src/monsters.c
@@ -24,7 +24,6 @@
 #include "monsters.h"
 
 #include "economy.h"
-#include "chaos.h"
 #include "give.h"
 #include "guard.h"
 #include "laws.h"
@@ -843,18 +842,14 @@ void plan_monsters(faction * f)
     pathfinder_cleanup();
 }
 
-static double chaosfactor(region * r)
-{
-    return fval(r, RF_CHAOTIC) ? ((double)(1 + get_chaoscount(r)) / 1000.0) : 0.0;
-}
-
 static int nrand(int handle_start, int sub)
 {
     int res = 0;
 
     do {
-        if (rng_int() % 100 < handle_start)
+        if (rng_int() % 100 < handle_start) {
             res++;
+        }
         handle_start -= sub;
     } while (handle_start > 0);
 
@@ -876,7 +871,7 @@ void spawn_dragons(void)
     region *r;
     faction *monsters = get_or_create_monsters();
     int minage = config_get_int("monsters.spawn.min_age", 100);
-    int spawn_chance = 100 * config_get_int("monsters.spawn.chance", 100);
+    int spawn_chance = config_get_int("monsters.spawn.chance", 100);
 
     if (spawn_chance <= 0) {
         /* monster spawning disabled */
@@ -895,7 +890,8 @@ void spawn_dragons(void)
         else if ((r->terrain == newterrain(T_GLACIER)
             || r->terrain == newterrain(T_SWAMP)
             || r->terrain == newterrain(T_DESERT))
-            && rng_int() % spawn_chance < (5 + 100 * chaosfactor(r))) {
+            && rng_int() % spawn_chance < 6)
+        {
             if (chance(0.80)) {
                 u = create_unit(r, monsters, nrand(60, 20) + 1, get_race(RC_FIREDRAGON), 0, NULL, NULL);
             }
@@ -907,7 +903,7 @@ void spawn_dragons(void)
 
             log_debug("spawning %d %s in %s.\n", u->number,
                 LOC(default_locale,
-                rc_name_s(u_race(u), (u->number == 1) ? NAME_SINGULAR : NAME_PLURAL)), regionname(r, NULL));
+                    rc_name_s(u_race(u), (u->number == 1) ? NAME_SINGULAR : NAME_PLURAL)), regionname(r, NULL));
 
             name_unit(u);
 
@@ -932,9 +928,9 @@ void spawn_undead(void)
                 continue;
             }
         }
-        /* Chance 0.1% * chaosfactor */
+
         if (r->land && unburied > rpeasants(r) / 20
-            && rng_int() % 10000 < (100 + 100 * chaosfactor(r))) {
+            && rng_int() % 10000 < 200) {
             message *msg;
             unit *u;
             /* es ist sinnfrei, wenn irgendwo im Wald 3er-Einheiten Untote entstehen.
diff --git a/storage b/storage
index 5623ee652..4d33d5081 160000
--- a/storage
+++ b/storage
@@ -1 +1 @@
-Subproject commit 5623ee6527e97af20c7d8efc03800b6fe1579744
+Subproject commit 4d33d5081f593272c25baa64d6210635c1c7e828

From af7e2919235f4afaf585669a3971c525dcee9ae1 Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Thu, 28 Jun 2018 23:03:12 +0200
Subject: [PATCH 16/62] submodule updates

---
 clibs   | 2 +-
 storage | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/clibs b/clibs
index bd70ef20b..9b6e34959 160000
--- a/clibs
+++ b/clibs
@@ -1 +1 @@
-Subproject commit bd70ef20babbe9949e5ad5acd5317e7d39ad1e72
+Subproject commit 9b6e34959f77d7ca3a4ce3826cb487487f557441
diff --git a/storage b/storage
index 4d33d5081..6eea76952 160000
--- a/storage
+++ b/storage
@@ -1 +1 @@
-Subproject commit 4d33d5081f593272c25baa64d6210635c1c7e828
+Subproject commit 6eea7695285f9c9f1d25421897ca55f466ef7566

From 6403c75c858a8becfed7ece8aecdcd2bae805f2c Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Mon, 2 Jul 2018 19:58:26 +0200
Subject: [PATCH 17/62] monsters should not be anonymous.

---
 src/kernel/faction.c | 30 ------------------------------
 src/monsters.c       |  5 +++++
 2 files changed, 5 insertions(+), 30 deletions(-)

diff --git a/src/kernel/faction.c b/src/kernel/faction.c
index 16c0c789c..b4dfe51ca 100755
--- a/src/kernel/faction.c
+++ b/src/kernel/faction.c
@@ -492,36 +492,6 @@ void destroyfaction(faction ** fp)
     }
 
     handle_event(f->attribs, "destroy", f);
-#if 0
-    faction *ff;
-    for (ff = factions; ff; ff = ff->next) {
-        group *g;
-        ally *sf, **sfp;
-
-        for (sfp = &ff->allies; *sfp;) {
-            sf = *sfp;
-            if (sf->faction == f || sf->faction == NULL) {
-                *sfp = sf->next;
-                free(sf);
-            }
-            else
-                sfp = &(*sfp)->next;
-        }
-        for (g = ff->groups; g; g = g->next) {
-            for (sfp = &g->allies; *sfp; ) {
-                sf = *sfp;
-                if (sf->faction == f || sf->faction == NULL) {
-                    *sfp = sf->next;
-                    free(sf);
-                }
-                else {
-                    sfp = &(*sfp)->next;
-                }
-            }
-        }
-    }
-#endif
-
     if (f->alliance) {
         setalliance(f, 0);
     }
diff --git a/src/monsters.c b/src/monsters.c
index 181615a40..bbeb483b8 100644
--- a/src/monsters.c
+++ b/src/monsters.c
@@ -755,6 +755,11 @@ void plan_monsters(faction * f)
             if (u->faction!=f)
                 continue;
 
+            /* Parteitarnung von Monstern ist doof: */
+            if (fval(u, UFL_ANON_FACTION)) {
+                u->flags &= ~UFL_ANON_FACTION;
+            }
+
             /* Befehle m�ssen jede Runde neu gegeben werden: */
             free_orders(&u->orders);
             if (skill_enabled(SK_PERCEPTION)) {

From 68f241ad7df8c22b4d803daaf8122e384a1c9e72 Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno@kn-bremen.de>
Date: Mon, 11 Jun 2018 21:00:01 +0200
Subject: [PATCH 18/62] redirect cron output to a logfile

---
 process/cron/preview.cron | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/process/cron/preview.cron b/process/cron/preview.cron
index 71ebd0571..eb39fd8f8 100755
--- a/process/cron/preview.cron
+++ b/process/cron/preview.cron
@@ -1,5 +1,5 @@
 #!/bin/bash
-
+(
 [ "$PREVIEW" != "yes" ] && exit
 [ -z ${ERESSEA} ] && ERESSEA=$HOME/eressea
 
@@ -13,3 +13,5 @@ for game in 2 3 4 ; do
 	${SRC}/s/preview -g ${game} run && \
 	${SRC}/s/preview -g ${game} send
 done
+) | tee -a $HOME/log/preview.cron.log
+

From e2c2f61927f43777888852cbf79b6d5b0540ba25 Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno@kn-bremen.de>
Date: Sat, 7 Jul 2018 10:45:11 +0200
Subject: [PATCH 19/62] pipe cron output to logfile, fix sendreport script

---
 process/Makefile              |  3 +--
 process/cron/run-eressea.cron |  4 +++-
 process/functions.sh          | 10 --------
 process/sendreport.sh         | 45 ++++++++++++++++++++---------------
 4 files changed, 30 insertions(+), 32 deletions(-)
 delete mode 100644 process/functions.sh

diff --git a/process/Makefile b/process/Makefile
index 8edfed0b8..0d84bdae2 100644
--- a/process/Makefile
+++ b/process/Makefile
@@ -1,6 +1,5 @@
 SCRIPTS=compress.sh send-bz2-report send-zip-report create-orders \
-run-turn sendreports.sh
-IGNORE=sendreport.sh
+run-turn sendreports.sh sendreport.sh
 
 shellcheck: $(SCRIPTS)
 	shellcheck $(SCRIPTS)
diff --git a/process/cron/run-eressea.cron b/process/cron/run-eressea.cron
index 4ea38184c..ecd4c6b11 100755
--- a/process/cron/run-eressea.cron
+++ b/process/cron/run-eressea.cron
@@ -1,6 +1,6 @@
 #!/bin/bash
 GAME=$1
-
+(
 [ "$ENABLED" != "yes" ] && exit
 [ -z ${ERESSEA} ] && ERESSEA=$HOME/eressea
 
@@ -58,3 +58,5 @@ $BIN/compress.sh $GAME $TURN
 $BIN/sendreports.sh $GAME
 $BIN/backup-eressea $GAME $TURN
 rm -f test/execute.lock
+) | tee -a $HOME/log/eressea.cron.log
+
diff --git a/process/functions.sh b/process/functions.sh
deleted file mode 100644
index 859fd302a..000000000
--- a/process/functions.sh
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/bin/bash
-
-PATH=$ERESSEA/bin:$PATH
-
-function abort() {
-  if [ $# -gt 0 ]; then 
-    echo $@
-  fi
-  exit -1
-}
diff --git a/process/sendreport.sh b/process/sendreport.sh
index be643ef56..d98ed505e 100755
--- a/process/sendreport.sh
+++ b/process/sendreport.sh
@@ -1,11 +1,17 @@
 #!/bin/bash
 
 
-if [ -z $ERESSEA ]; then
+if [ -z "$ERESSEA" ]; then
   echo "You have to define the \$ERESSEA environment variable to run $0"
   exit -2
 fi
-source $HOME/bin/functions.sh
+
+function abort() {
+  if [ $# -gt 0 ]; then
+    echo "$@"
+  fi
+  exit -1
+}
 
 GAME=$1
 EMAIL=$2
@@ -14,32 +20,33 @@ PASSWD=$4
 #echo "$GAME $EMAIL $FACTION $PASSWD" >> /tmp/report.log
 
 function reply() {
-  echo $@ | mutt -s "Reportnachforderung Partei ${FACTION}" $EMAIL
-  abort $@
+  echo "$@" | mutt -s "Reportnachforderung Partei ${FACTION}" "$EMAIL"
+  abort "$@"
 }
 
-LOCKFILE=$ERESSEA/.report.lock
-[ -e $LOCKFILE ] && reply "lockfile exists. wait for mail delivery to finish."
+LOCKFILE="$ERESSEA/.report.lock"
+[ -e "$LOCKFILE" ] && reply "lockfile exists. wait for mail delivery to finish."
 
-REPLYTO='accounts@eressea.de'
+echo "$(date):report:$GAME:$EMAIL:$FACTION:$PASSWD" >> "$ERESSEA/request.log"
 
-echo `date`:report:$GAME:$EMAIL:$FACTION:$PASSWD >> $ERESSEA/request.log
+cd "$ERESSEA" || exit
+checkpasswd.py "game-$GAME/passwd" "$FACTION" "$PASSWD" || reply "Das Passwort fuer die Partei $FACTION ist ungueltig"
 
-cd $ERESSEA
-checkpasswd.py game-$GAME/passwd $FACTION $PASSWD || reply "Das Passwort fuer die Partei $FACTION ist ungueltig"
-
-cd $ERESSEA/game-$GAME/reports
-if [ ! -e ${FACTION}.sh ]; then
+cd "$ERESSEA/game-$GAME/reports" || exit
+if [ ! -e "${FACTION}.sh" ]; then
   echo "Der Report für Partei $FACTION kann wegen technischer Probleme leider nicht nachgefordert werden: No such file ${FACTION}.sh" \
-  | mutt -s "Reportnachforderung Partei ${FACTION}" $EMAIL
+  | mutt -s "Reportnachforderung Partei ${FACTION}" "$EMAIL"
   exit
 fi
 
-source ${FACTION}.sh $EMAIL || reply "Unbekannte Partei $FACTION"
+bash "${FACTION}.sh" "$EMAIL" || reply "Unbekannte Partei $FACTION"
 
-if [ -e $ERESSEA/game-$GAME/eressea.db ]; then
+if [ -e "$ERESSEA/game-$GAME/eressea.db" ]; then
   SQL="select email from faction f left join faction_data fd on fd.faction_id=f.id where f.game_id=$GAME AND fd.code='$FACTION' and fd.turn=(select max(turn) from faction_data fx where fx.faction_id=f.id)"
-  OWNER=$(sqlite3 $ERESSEA/game-$GAME/eressea.db "$SQL")
-  echo "Der Report Deiner Partei wurde an ${EMAIL} gesandt." \
-  | mutt -s "Reportnachforderung Partei ${FACTION}" $OWNER
+  OWNER=$(sqlite3 "$ERESSEA/game-$GAME/eressea.db" "$SQL")
+  if [ ! -z "$OWNER" ]; then
+    echo "Der Report Deiner Partei wurde an ${EMAIL} gesandt." \
+    | mutt -s "Reportnachforderung Partei ${FACTION}" "$OWNER"
+  fi
 fi
+

From b1b4654b0eb0736497dcd82c192a7ca34f1e4b63 Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Sat, 7 Jul 2018 12:21:42 +0200
Subject: [PATCH 20/62] Bug 2445: duplicate strings warnings.

---
 conf/e2/locales.json              |  2 ++
 res/translations/strings-e2.de.po | 49 +++++++++++++++++++++++++++++++
 res/translations/strings-e2.en.po | 49 +++++++++++++++++++++++++++++++
 res/translations/strings.de.po    | 36 -----------------------
 res/translations/strings.en.po    | 36 -----------------------
 5 files changed, 100 insertions(+), 72 deletions(-)
 create mode 100644 res/translations/strings-e2.de.po
 create mode 100644 res/translations/strings-e2.en.po

diff --git a/conf/e2/locales.json b/conf/e2/locales.json
index 21e91755f..3376cfedc 100644
--- a/conf/e2/locales.json
+++ b/conf/e2/locales.json
@@ -1,7 +1,9 @@
 {
     "include": [
       "config://res/translations/strings.de.po",
+      "config://res/translations/strings-e2.de.po",
       "config://res/translations/strings.en.po",
+      "config://res/translations/strings-e2.en.po",
       "config://res/translations/messages.de.po",
       "config://res/translations/messages.en.po",
       "config://res/core/messages.xml"
diff --git a/res/translations/strings-e2.de.po b/res/translations/strings-e2.de.po
new file mode 100644
index 000000000..fde4244af
--- /dev/null
+++ b/res/translations/strings-e2.de.po
@@ -0,0 +1,49 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: \n"
+"POT-Creation-Date: \n"
+"PO-Revision-Date: \n"
+"Last-Translator: \n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: de\n"
+"X-Generator: Poedit 2.0.7\n"
+
+msgctxt "spellinfo"
+msgid "concealing_aura"
+msgstr "Dieser Zauber wird die gesamte Ausrüstung der Zieleinheit für einige Zeit vor den Blicken anderer verschleiern."
+
+msgctxt "spellinfo"
+msgid "raindance"
+msgstr "Dieses uralte Tanzritual ruft die Kräfte des Lebens und der Fruchtbarkeit. Die Erträge der Bauern werden für einige Wochen deutlich besser ausfallen."
+
+msgctxt "spellinfo"
+msgid "earn_silver#gwyrrd"
+msgstr "Die Fähigkeiten der Gwyrrd-Magier in der Viehzucht und Heilung sind bei den Bauern sehr begehrt. Gerade auf Märkten sind ihre Dienste häufig sehr gefragt. Manch einer mag auch sein Talent dazu nutzen, ein Tier für einen besseren Preis zu verkaufen. Pro Stufe kann der Magier so 50 Silber verdienen."
+
+msgctxt "spellinfo"
+msgid "earn_silver#draig"
+msgstr "In den dunkleren Gassen gibt es sie, die Flüche und Verhexungen auf Bestellung. Aber auch Gegenzauber hat der Jünger des Draigs natürlich im Angebot. Ob nun der Sohn des Nachbarn in einen Liebesbann gezogen werden soll oder die Nebenbuhlerin Pickel und Warzen bekommen soll, niemand gibt gerne zu, zu solchen Mitteln gegriffen zu haben. Für diese Dienstleistung streicht der Magier 50 Silber pro Stufe ein."
+
+msgctxt "spellinfo"
+msgid "earn_silver#cerddor"
+msgstr "Cerddormagier sind die führenden Gaukler unter den Magiern, sie lieben es das Volk zu unterhalten und im Mittelpunkt zu stehen. Schon Anfänger lernen die kleinen Kunststücke und magischen Tricks, mit denen man das Volk locken und verführen kann, den Geldbeutel ganz weit zu öffnen, und am Ende der Woche wird der Gaukler 50 Silber pro Stufe verdient haben."
+
+msgctxt "spellinfo"
+msgid "earn_silver#tybied"
+msgstr "Wenn einem der Alchemist nicht weiterhelfen kann, geht man zu dem gelehrten Tybiedmagier. Seine Tränke und Tinkturen helfen gegen alles, was man sonst nicht bekommen kann. Ob nun die kryptische Formel unter dem Holzschuh des untreuen Ehemannes wirklich geholfen hat - nun, der des Lesens nicht mächtige Bauer wird es nie wissen. Dem Magier hilft es auf jeden Fall... beim Füllen seines Geldbeutels. 50 Silber pro Stufe lassen sich so in einer Woche verdienen."
+
+msgctxt "spellinfo"
+msgid "earn_silver#illaun"
+msgstr "Niemand kann so gut die Träume deuten wie ein Magier des Illaun. Auch die Kunst der Wahrsagerei, des Kartenlegens und des Handlesens sind ihm geläufig. Dafür zahlen ihm die Bauern 50 Silber pro Stufe."
+
+msgctxt "describe"
+msgid "lifepotion"
+msgstr "Das 'Wasser des Lebens' ist in der Lage, aus gefällten Baumstämmen wieder lebende Bäume zu machen. Dazu wird ein knotiger Saugwurz zusammen mit einem Elfenlieb erwärmt, so dass man gerade noch den Finger reinhalten kann. Dies gieße man in ein Gefäß und lasse es langsam abkühlen. Der Extrakt reicht für 10 Holzstämme."
+
+msgctxt "spellinfo"
+msgid "blessedharvest"
+msgstr "Dieses Ernteritual verbessert die Erträge der arbeitenden Bauern in der Region um ein Silberstück. Je mehr Kraft der Druide investiert, desto länger wirkt der Zauber."
+
diff --git a/res/translations/strings-e2.en.po b/res/translations/strings-e2.en.po
new file mode 100644
index 000000000..3867de0f7
--- /dev/null
+++ b/res/translations/strings-e2.en.po
@@ -0,0 +1,49 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: \n"
+"POT-Creation-Date: \n"
+"PO-Revision-Date: \n"
+"Last-Translator: \n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: en\n"
+"X-Generator: Poedit 2.0.7\n"
+
+msgctxt "spellinfo"
+msgid "earn_silver#gwyrrd"
+msgstr "The abilities of the mages of Gwyrrd concerning the breeding and healing of cattle are highly appreciated among the peasants. Especially at the markets, their services are demanded frequently. Some of them also use their talents to sell an animal at a higher price. A magician can earn 50 silver pieces per level in this way."
+
+msgctxt "spellinfo"
+msgid "earn_silver#draig"
+msgstr "In the dark alleys you can find those who sell curses and hexes on demand - but you can buy the apropriate counterspells from the followers of Draig as well. May it be a love spell for the son of a neighbour or a wart in the face of a rival. For offering these services, the sorcerer charges 50 silver pieces per level."
+
+msgctxt "spellinfo"
+msgid "earn_silver#cerddor"
+msgstr "The mages of Cerddor truly are the bards of the wizards; they love to use their sorcery to entertain the crowds and to be the center of attention. Even the apprentices study those little magic tricks, which attract and fascinate the people and thus ensnare them into leaving a few coins or more for the artist. By the end of the week, the bard will have earned 50 silver per level."
+
+msgctxt "spellinfo"
+msgid "earn_silver#illaun"
+msgstr "No one can read dreams as well as the mages of Illaun. Furthermore, they are also familiar with all other common means of foretelling the future like crystal balls, tarot cards or palms. A mentalist can earn 50 silver pieces per level and week for offering these services to peasants."
+
+msgctxt "spellinfo"
+msgid "earn_silver#tybied"
+msgstr "If the local alchemist could not help you, you should visit a scholar of Tybied. His potions and tinctures may help when nothing else does. If the cryptic formula under the wooden shoes of the unfaithful husband really helped? - well, the peasant, who isn't capable of reading, will never know. At least it helped the magician... to fill his purse. In one week he can earn 50 silver per level that way."
+
+msgctxt "spellinfo"
+msgid "blessedharvest"
+msgstr "This ritual increases the output of the local farms. Peasants in the region produce an extra silverpiece. The stronger the druid's spell is, the longer the effect will last."
+
+msgctxt "spellinfo"
+msgid "raindance"
+msgstr "This ancient rite calls upon the forces of life and fertility. For the next few weeks, the peasant's harvest will be extraordinary good."
+
+msgctxt "spellinfo"
+msgid "concealing_aura"
+msgstr "This spell will hide the whole equipment of a target unit from the looks of others."
+
+msgctxt "describe"
+msgid "lifepotion"
+msgstr "The \"Water of Life\" allows living trees to be created from logs. A Knotroot and Elvendear are heated until one can just still keep one's finger in. This is then poured into a jar and allowed to cool slowly. The extract is sufficient for 10 pieces of wood."
+
diff --git a/res/translations/strings.de.po b/res/translations/strings.de.po
index afcceb94b..1731fa10e 100644
--- a/res/translations/strings.de.po
+++ b/res/translations/strings.de.po
@@ -327,10 +327,6 @@ msgctxt "spell"
 msgid "clone"
 msgstr "Seelenkopie"
 
-msgctxt "spellinfo"
-msgid "concealing_aura"
-msgstr "Dieser Zauber wird die gesamte Ausrüstung der Zieleinheit für einige Zeit vor den Blicken anderer verschleiern."
-
 msgctxt "shipinfo"
 msgid "balloon"
 msgstr "Der Sumpfgasballon besteht aus einem großen Weidenkorb, welcher Platz für maximal 5 Personen oder 500 Gewichtseinheiten bietet, und einer großen, mit Sumpfgas gefüllten Wyrmblase. Bei guten Winden kann sich der Ballon zwei Regionen pro Woche fortbewegen. Das Führen eines Ballons ist nicht einfach, und der Kapitän muss mindestens ein Segeltalent von 6 besitzen. Diese neue Entwicklung auf Eressea wird ausschließlich für den Xontormia-Expreß hergestellt und die Baupläne sind streng geheim. So ist es auch bisher noch niemandem gelungen, ein Exemplar nachzubauen."
@@ -908,10 +904,6 @@ msgctxt "spellinfo"
 msgid "disturbingdreams"
 msgstr "Dieser Zauber führt in der betroffenen Region für einige Wochen zu Schlaflosigkeit und Unruhe. Den Betroffenen fällt das Lernen deutlich schwerer."
 
-msgctxt "spellinfo"
-msgid "raindance"
-msgstr "Dieses uralte Tanzritual ruft die Kräfte des Lebens und der Fruchtbarkeit. Die Erträge der Bauern werden für einige Wochen deutlich besser ausfallen."
-
 msgid "wdw_pyramid"
 msgstr "Pyramide"
 
@@ -1459,10 +1451,6 @@ msgctxt "race"
 msgid "goblin_x"
 msgstr "Goblin"
 
-msgctxt "spellinfo"
-msgid "earn_silver#gwyrrd"
-msgstr "Die Fähigkeiten der Gwyrrd-Magier in der Viehzucht und Heilung sind bei den Bauern sehr begehrt. Gerade auf Märkten sind ihre Dienste häufig sehr gefragt. Manch einer mag auch sein Talent dazu nutzen, ein Tier für einen besseren Preis zu verkaufen. Pro Stufe kann der Magier so 50 Silber verdienen."
-
 msgctxt "spell"
 msgid "raindance"
 msgstr "Regentanz"
@@ -1494,10 +1482,6 @@ msgstr "Mallorn"
 msgid "mallorn_p"
 msgstr "Mallorn"
 
-msgctxt "describe"
-msgid "lifepotion"
-msgstr "Das 'Wasser des Lebens' ist in der Lage, aus gefällten Baumstämmen wieder lebende Bäume zu machen. Dazu wird ein knotiger Saugwurz zusammen mit einem Elfenlieb erwärmt, so dass man gerade noch den Finger reinhalten kann. Dies gieße man in ein Gefäß und lasse es langsam abkühlen. Der Extrakt reicht für 10 Holzstämme."
-
 msgctxt "spell"
 msgid "homestone"
 msgstr "Heimstein"
@@ -1740,10 +1724,6 @@ msgstr "Beschwörung eines Wasserelementares"
 msgid "GEBAEUDE"
 msgstr "GEBÄUDE"
 
-msgctxt "spellinfo"
-msgid "blessedharvest"
-msgstr "Dieses Ernteritual verbessert die Erträge der arbeitenden Bauern in der Region um ein Silberstück. Je mehr Kraft der Druide investiert, desto länger wirkt der Zauber."
-
 msgid "KRAEUTER"
 msgstr "KRÄUTER"
 
@@ -3147,10 +3127,6 @@ msgctxt "spellinfo"
 msgid "create_focus"
 msgstr "Erzeugt einen Aurafokus."
 
-msgctxt "spellinfo"
-msgid "earn_silver#draig"
-msgstr "In den dunkleren Gassen gibt es sie, die Flüche und Verhexungen auf Bestellung. Aber auch Gegenzauber hat der Jünger des Draigs natürlich im Angebot. Ob nun der Sohn des Nachbarn in einen Liebesbann gezogen werden soll oder die Nebenbuhlerin Pickel und Warzen bekommen soll, niemand gibt gerne zu, zu solchen Mitteln gegriffen zu haben. Für diese Dienstleistung streicht der Magier 50 Silber pro Stufe ein."
-
 msgctxt "spell"
 msgid "song_of_slavery"
 msgstr "Gesang der Versklavung"
@@ -4910,10 +4886,6 @@ msgstr "Elixier der Macht"
 msgid "wand_p"
 msgstr "Zauberstäbe"
 
-msgctxt "spellinfo"
-msgid "earn_silver#cerddor"
-msgstr "Cerddormagier sind _die_ Gaukler unter den Magiern, sie lieben es das Volk zu unterhalten und im Mittelpunkt zu stehen. Schon Anfänger lernen die kleinen Kunststücke und magischen Tricks, mit denen man das Volk locken und verführen kann, den Geldbeutel ganz weit zu öffnen, und am Ende der Woche wird der Gaukler 50 Silber pro Stufe verdient haben."
-
 msgctxt "spell"
 msgid "strength"
 msgstr "Unbekannter Effekt"
@@ -4996,10 +4968,6 @@ msgstr "Myrrhe"
 msgid "skeleton_prefix_9"
 msgstr "Dunkle"
 
-msgctxt "spellinfo"
-msgid "earn_silver#illaun"
-msgstr "Niemand kann so gut die Träume deuten wie ein Magier des Illaun. Auch die Kunst der Wahrsagerei, des Kartenlegens und des Handlesens sind ihm geläufig. Dafür zahlen ihm die Bauern 50 Silber pro Stufe."
-
 msgctxt "spell"
 msgid "big_recruit"
 msgstr "Hohe Kunst der Überzeugung"
@@ -5913,10 +5881,6 @@ msgctxt "spell"
 msgid "cerddorfumbleshield"
 msgstr "Bannlied"
 
-msgctxt "spellinfo"
-msgid "earn_silver#tybied"
-msgstr "Wenn einem der Alchemist nicht weiterhelfen kann, geht man zu dem gelehrten Tybiedmagier. Seine Tränke und Tinkturen helfen gegen alles, was man sonst nicht bekommen kann. Ob nun die kryptische Formel unter dem Holzschuh des untreuen Ehemannes wirklich geholfen hat - nun, der des Lesens nicht mächtige Bauer wird es nie wissen. Dem Magier hilft es auf jeden Fall... beim Füllen seines Geldbeutels. 50 Silber pro Stufe lassen sich so in einer Woche verdienen."
-
 msgid "ANZAHL"
 msgstr "ANZAHL"
 
diff --git a/res/translations/strings.en.po b/res/translations/strings.en.po
index c8bd637db..0d64e9f99 100644
--- a/res/translations/strings.en.po
+++ b/res/translations/strings.en.po
@@ -273,10 +273,6 @@ msgctxt "spell"
 msgid "clone"
 msgstr "Doppelganger"
 
-msgctxt "spellinfo"
-msgid "concealing_aura"
-msgstr "This spell will hide the whole equipment of a target unit from the looks of others."
-
 msgctxt "spell"
 msgid "bloodsacrifice"
 msgstr "Lesser Sacrifice"
@@ -695,10 +691,6 @@ msgctxt "spellinfo"
 msgid "disturbingdreams"
 msgstr "This spell causes insomnia and restlessness in a whole region for several weeks. All affected persons will learn much slower than normal."
 
-msgctxt "spellinfo"
-msgid "raindance"
-msgstr "This ancient rite calls upon the forces of life and fertility. For the next few weeks, the peasant's harvest will be extraordinary good."
-
 msgid "wdw_pyramid"
 msgstr "pyramid"
 
@@ -1221,10 +1213,6 @@ msgctxt "race"
 msgid "goblin_x"
 msgstr "goblin"
 
-msgctxt "spellinfo"
-msgid "earn_silver#gwyrrd"
-msgstr "The abilities of the mages of Gwyrrd concerning the breeding and healing of cattle are highly appreciated among the peasants. Especially at the markets, their services are demanded frequently. Some of them also use their talents to sell an animal at a higher price. A magician can earn 50 silver pieces per level in this way."
-
 msgctxt "spell"
 msgid "raindance"
 msgstr "Rain Dance"
@@ -1252,10 +1240,6 @@ msgstr "mallorn"
 msgid "mallorn_p"
 msgstr "mallorn"
 
-msgctxt "describe"
-msgid "lifepotion"
-msgstr "The \"Water of Life\" allows living trees to be created from logs. A Knotroot and Elvendear are heated until one can just still keep one's finger in. This is then poured into a jar and allowed to cool slowly. The extract is sufficient for 10 pieces of wood."
-
 msgid "thickfog_trail"
 msgstr "%s"
 
@@ -1490,10 +1474,6 @@ msgctxt "spell"
 msgid "goodwinds"
 msgstr "Summon Water Elemental"
 
-msgctxt "spellinfo"
-msgid "blessedharvest"
-msgstr "This ritual increases the output of the local farms. Peasants in the region produce an extra silverpiece. The stronger the druid's spell is, the longer the effect will last."
-
 msgid "KRAEUTER"
 msgstr "HERBS"
 
@@ -2732,10 +2712,6 @@ msgctxt "spellinfo"
 msgid "create_focus"
 msgstr "Creates an aurafocus crystal."
 
-msgctxt "spellinfo"
-msgid "earn_silver#draig"
-msgstr "In the dark alleys you can find those who sell curses and hexes on demand - but you can buy the apropriate counterspells from the followers of Draig as well. May it be a love spell for the son of a neighbour or a wart in the face of a rival. For offering these services, the sorcerer charges 50 silver pieces per level."
-
 msgctxt "spell"
 msgid "song_of_slavery"
 msgstr "Song of Slavery"
@@ -4370,10 +4346,6 @@ msgstr "elixir of power"
 msgid "wand_p"
 msgstr "wands"
 
-msgctxt "spellinfo"
-msgid "earn_silver#cerddor"
-msgstr "The mages of Cerddor truly are the bards of the wizards; they love to use their sorcery to entertain the crowds and to be the center of attention. Even the apprentices study those little magic tricks, which attract and fascinate the people and thus ensnare them into leaving a few coins or more for the artist. By the end of the week, the bard will have earned 50 silver per level."
-
 msgctxt "spell"
 msgid "strength"
 msgstr "Unknown Effect"
@@ -4426,10 +4398,6 @@ msgstr "trees"
 msgid "myrrh_p"
 msgstr "myrrh"
 
-msgctxt "spellinfo"
-msgid "earn_silver#illaun"
-msgstr "No one can read dreams as well as the mages of Illaun. Furthermore, they are also familiar with all other common means of foretelling the future like crystal balls, tarot cards or palms. A mentalist can earn 50 silver pieces per level and week for offering these services to peasants."
-
 msgctxt "spell"
 msgid "big_recruit"
 msgstr "High art of persuasion"
@@ -5222,10 +5190,6 @@ msgctxt "spell"
 msgid "cerddorfumbleshield"
 msgstr "Countersong"
 
-msgctxt "spellinfo"
-msgid "earn_silver#tybied"
-msgstr "If the local alchemist could not help you, you should visit a scholar of Tybied. His potions and tinctures may help when nothing else does. If the cryptic formula under the wooden shoes of the unfaithful husband really helped? - well, the peasant, who isn't capable of reading, will never know. At least it helped the magician... to fill his purse. In one week he can earn 50 silver per level that way."
-
 msgctxt "keyword"
 msgid "teach"
 msgstr "TEACH"

From 26e06804d677fe77e94ede1174d61652dc48fe13 Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Thu, 5 Jul 2018 20:06:32 +0200
Subject: [PATCH 21/62] autostudy framework

---
 src/CMakeLists.txt  |  1 +
 src/automate.c      | 71 +++++++++++++++++++++++++++++++++++++++++++++
 src/automate.h      | 28 ++++++++++++++++++
 src/kernel/config.c |  3 +-
 src/kernel/order.c  | 19 +++++++++---
 src/kernel/types.h  |  1 +
 src/kernel/unit.c   |  5 ++--
 src/kernel/unit.h   |  2 +-
 src/keyword.c       |  1 +
 src/keyword.h       |  1 +
 src/laws.c          |  2 ++
 src/study.c         |  8 ++---
 src/study.h         |  1 +
 13 files changed, 130 insertions(+), 13 deletions(-)
 create mode 100644 src/automate.c
 create mode 100644 src/automate.h

diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 5b267c0d5..0ff92bf2d 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -86,6 +86,7 @@ ENDIF()
 
 set (ERESSEA_SRC
   vortex.c
+  automate.c
   move.c
   piracy.c
   spells.c
diff --git a/src/automate.c b/src/automate.c
new file mode 100644
index 000000000..0855ff0e3
--- /dev/null
+++ b/src/automate.c
@@ -0,0 +1,71 @@
+#include <platform.h>
+
+#include "kernel/faction.h"
+#include "kernel/order.h"
+#include "kernel/region.h"
+#include "kernel/unit.h"
+
+#include "util/log.h"
+
+#include "automate.h"
+#include "keyword.h"
+#include "study.h"
+
+#include <stdlib.h>
+
+typedef struct student {
+    unit *u;
+    skill_t sk;
+    int level;
+} student;
+
+#define MAXSTUDENTS 128
+
+int cmp_students(const void *lhs, const void *rhs) {
+    const student *a = (const student *)lhs;
+    const student *b = (const student *)rhs;
+    if (a->sk == b->sk) {
+        /* sort by level, descending: */
+        return b->level - a->level;
+    }
+    /* order by skill */
+    return (int)a->sk - (int)b->sk;
+}
+
+void do_autostudy(region *r) {
+    unit *u;
+    int nstudents = 0;
+    student students[MAXSTUDENTS];
+
+    for (u = r->units; u; u = u->next) {
+        keyword_t kwd = getkeyword(u->thisorder);
+        if (kwd == K_AUTOSTUDY) {
+            student * st = students + nstudents;
+            if (++nstudents == MAXSTUDENTS) {
+                log_fatal("you must increase MAXSTUDENTS");
+            }
+            st->u = u;
+            init_order(u->thisorder, u->faction->locale);
+            st->sk = getskill(u->faction->locale);
+            st->level = effskill_study(u, st->sk);
+        }
+    }
+    if (nstudents > 0) {
+        int i, taught = 0;
+        skill_t sk = NOSKILL;
+        student *teacher = NULL, *student = NULL;
+
+        qsort(students, nstudents, sizeof(student), cmp_students);
+        for (i = 0; i != nstudents; ++i) {
+            if (students[i].u) {
+                if (sk == NOSKILL) {
+                    sk = students[i].sk;
+                }
+                else if (sk != students[i].sk) {
+                    continue;
+                }
+                students[i].u = NULL;
+            }
+        }
+    }
+}
diff --git a/src/automate.h b/src/automate.h
new file mode 100644
index 000000000..ded8f938b
--- /dev/null
+++ b/src/automate.h
@@ -0,0 +1,28 @@
+/*
+Copyright (c) 1998-2018, Enno Rehling <enno@eressea.de>
+Katja Zedel <katze@felidae.kn-bremen.de
+Christian Schlittchen <corwin@amber.kn-bremen.de>
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+**/
+
+#pragma once
+
+#ifndef H_GC_AUTOMATE
+#define H_GC_AUTOMATE
+
+struct region;
+
+void do_autostudy(struct region *r);
+
+#endif
diff --git a/src/kernel/config.c b/src/kernel/config.c
index ec45ddd0d..750d406f6 100644
--- a/src/kernel/config.c
+++ b/src/kernel/config.c
@@ -135,7 +135,8 @@ const char *parameters[MAXPARAMS] = {
     "GRUPPE",
     "PARTEITARNUNG",
     "BAEUME",
-    "ALLIANZ"
+    "ALLIANZ",
+    "AUTO"
 };
 
 int findoption(const char *s, const struct locale *lang)
diff --git a/src/kernel/order.c b/src/kernel/order.c
index e660edf81..ac2573758 100644
--- a/src/kernel/order.c
+++ b/src/kernel/order.c
@@ -214,12 +214,12 @@ static int create_data(keyword_t kwd, const char *s,
     order_data *data;
     int id;
 
-    assert(kwd!=NOKEYWORD);
+    assert(kwd != NOKEYWORD);
 
     if (!s || *s == 0) {
         return 0;
     }
-    if (kwd==K_STUDY) {
+    if (kwd == K_STUDY || kwd == K_AUTOSTUDY) {
         const char * sptr = s;
         skill_t sk = get_skill(parse_token_depr(&sptr), lang);
         if (sk != SK_MAGIC && sk != NOSKILL) {
@@ -332,6 +332,14 @@ order *parse_order(const char *s, const struct locale * lang)
                 sptr = sp;
             }
         }
+        else if (kwd == K_STUDY) {
+            const char *sp = sptr;
+            p = parse_token_depr(&sp);
+            if (p && isparam(p, lang, P_AUTO)) {
+                kwd = K_AUTOSTUDY;
+                sptr = sp;
+            }
+        }
         if (kwd != NOKEYWORD) {
             order *ord = (order *)malloc(sizeof(order));
             create_order_i(ord, kwd, sptr, persistent, noerror, lang);
@@ -366,6 +374,7 @@ bool is_repeated(keyword_t kwd)
     case K_STEAL:
     case K_SABOTAGE:
     case K_STUDY:
+    case K_AUTOSTUDY:
     case K_TEACH:
     case K_GROW:
     case K_PLANT:
@@ -406,6 +415,7 @@ bool is_exclusive(const order * ord)
     case K_STEAL:
     case K_SABOTAGE:
     case K_STUDY:
+    case K_AUTOSTUDY:
     case K_TEACH:
     case K_GROW:
     case K_PLANT:
@@ -447,6 +457,7 @@ bool is_long(keyword_t kwd)
     case K_STEAL:
     case K_SABOTAGE:
     case K_STUDY:
+    case K_AUTOSTUDY:
     case K_TEACH:
     case K_GROW:
     case K_PLANT:
@@ -541,7 +552,7 @@ keyword_t init_order(const struct order *ord, const struct locale *lang)
 
             assert(sk < MAXSKILLS);
             assert(lang);
-            assert(kwd == K_STUDY);
+            assert(kwd == K_STUDY || kwd == K_AUTOSTUDY);
             str = skillname(sk, lang);
             if (strchr(str, ' ') == NULL) {
                 init_tokens_str(str);
@@ -575,7 +586,7 @@ keyword_t init_order_depr(const struct order *ord)
 {
     if (ord) {
         keyword_t kwd = ORD_KEYWORD(ord);
-        assert(kwd != K_STUDY);
+        assert(kwd != K_STUDY && kwd != K_AUTOSTUDY);
     }
     return init_order(ord, NULL);
 }
diff --git a/src/kernel/types.h b/src/kernel/types.h
index 61f1268cc..48a4326fa 100644
--- a/src/kernel/types.h
+++ b/src/kernel/types.h
@@ -130,6 +130,7 @@ typedef enum {
   P_FACTIONSTEALTH,
   P_TREES,
   P_ALLIANCE,
+  P_AUTO,
   MAXPARAMS,
   NOPARAM 
 } param_t;
diff --git a/src/kernel/unit.c b/src/kernel/unit.c
index 07c6c0856..770723bea 100644
--- a/src/kernel/unit.c
+++ b/src/kernel/unit.c
@@ -1310,13 +1310,12 @@ int eff_skill(const unit * u, const skill *sv, const region *r)
     return 0;
 }
 
-int effskill_study(const unit * u, skill_t sk, const region * r)
+int effskill_study(const unit * u, skill_t sk)
 {
     skill *sv = unit_skill(u, sk);
     if (sv && sv->level > 0) {
         int mlevel = sv->level;
-        if (!r) r = u->region;
-        mlevel += get_modifier(u, sv->id, sv->level, r, true);
+        mlevel += get_modifier(u, sv->id, sv->level, u->region, true);
         if (mlevel > 0)
             return mlevel;
     }
diff --git a/src/kernel/unit.h b/src/kernel/unit.h
index ac61affde..a6135715c 100644
--- a/src/kernel/unit.h
+++ b/src/kernel/unit.h
@@ -163,7 +163,7 @@ extern "C" {
     void clone_men(const struct unit *src, struct unit *dst, int n); /* like transfer, but do not subtract from src */
 
     int eff_skill(const struct unit *u, const struct skill *sv, const struct region *r);
-    int effskill_study(const struct unit *u, skill_t sk, const struct region *r);
+    int effskill_study(const struct unit *u, skill_t sk);
 
     int get_modifier(const struct unit *u, skill_t sk, int level,
         const struct region *r, bool noitem);
diff --git a/src/keyword.c b/src/keyword.c
index 7519b6449..48726bcbc 100644
--- a/src/keyword.c
+++ b/src/keyword.c
@@ -149,5 +149,6 @@ const char *keywords[MAXKEYWORDS] = {
     "promote",
     "pay",
     "loot",
+    "autostudy",
 };
 
diff --git a/src/keyword.h b/src/keyword.h
index a9273c3f5..6e4832708 100644
--- a/src/keyword.h
+++ b/src/keyword.h
@@ -72,6 +72,7 @@ extern "C"
         K_PROMOTION,
         K_PAY,
         K_LOOT,
+        K_AUTOSTUDY,
         MAXKEYWORDS,
         NOKEYWORD
     } keyword_t;
diff --git a/src/laws.c b/src/laws.c
index aa6e237e2..3902e9a8e 100644
--- a/src/laws.c
+++ b/src/laws.c
@@ -26,6 +26,7 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 #include <modules/gmcmd.h>
 
 #include "alchemy.h"
+#include "automate.h"
 #include "battle.h"
 #include "economy.h"
 #include "keyword.h"
@@ -4011,6 +4012,7 @@ void init_processor(void)
     }
 
     p += 10;
+    add_proc_region(p, do_autostudy, "study automation");
     add_proc_order(p, K_TEACH, teach_cmd, PROC_THISORDER | PROC_LONGORDER,
         "Lehren");
     p += 10;
diff --git a/src/study.c b/src/study.c
index 46da90b74..6df5b5eaf 100644
--- a/src/study.c
+++ b/src/study.c
@@ -69,7 +69,7 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 #define TEACH_ALL 1
 #define TEACH_FRIENDS
 
-static skill_t getskill(const struct locale *lang)
+skill_t getskill(const struct locale *lang)
 {
     char token[128];
     const char * s = gettoken(token, sizeof(token));
@@ -331,7 +331,7 @@ int teach_cmd(unit * teacher, struct order *ord)
                         sk = teachskill[t];
                     }
                     if (sk != NOSKILL
-                        && effskill_study(teacher, sk, 0) - TEACHDIFFERENCE > effskill_study(student, sk, 0)) {
+                        && effskill_study(teacher, sk) - TEACHDIFFERENCE > effskill_study(student, sk)) {
                         teaching -= teach_unit(teacher, student, teaching, sk, true, &academy_students);
                     }
                 }
@@ -343,7 +343,7 @@ int teach_cmd(unit * teacher, struct order *ord)
                     init_order(student->thisorder, student->faction->locale);
                     sk = getskill(student->faction->locale);
                     if (sk != NOSKILL
-                        && effskill_study(teacher, sk, 0) - TEACHDIFFERENCE >= effskill(student, sk, 0)) {
+                        && effskill_study(teacher, sk) - TEACHDIFFERENCE >= effskill(student, sk)) {
                         teaching -= teach_unit(teacher, student, teaching, sk, true, &academy_students);
                     }
                 }
@@ -432,7 +432,7 @@ int teach_cmd(unit * teacher, struct order *ord)
                 continue;
             }
 
-            if (effskill_study(student, sk, 0) > effskill_study(teacher, sk, 0)
+            if (effskill_study(student, sk, NULL) > effskill_study(teacher, sk)
                 - TEACHDIFFERENCE) {
                 if (feedback) {
                     ADDMSG(&teacher->faction->msgs, msg_feedback(teacher, ord, "teach_asgood",
diff --git a/src/study.h b/src/study.h
index b4b76bb8c..e99fb4808 100644
--- a/src/study.h
+++ b/src/study.h
@@ -33,6 +33,7 @@ extern "C" {
     int study_cmd(struct unit *u, struct order *ord);
 
     magic_t getmagicskill(const struct locale *lang);
+    skill_t getskill(const struct locale *lang);
     bool is_migrant(struct unit *u);
     int study_cost(struct unit *u, skill_t talent);
 

From a93d8fd56b08787376ace7df8904ad36b3f295ab Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Sat, 7 Jul 2018 17:29:22 +0200
Subject: [PATCH 22/62] finish parser for K_AUTOSTUDY and P_AUTO.

---
 src/CMakeLists.txt      |  1 +
 src/automate.c          | 26 +++++++++++++-------------
 src/automate.h          | 12 ++++++++++++
 src/automate.test.c     | 38 ++++++++++++++++++++++++++++++++++++++
 src/kernel/order.c      |  2 +-
 src/kernel/order.test.c | 31 ++++++++++++++++++++++++++++---
 src/study.c             |  4 ++--
 src/test_eressea.c      |  3 ++-
 8 files changed, 97 insertions(+), 20 deletions(-)
 create mode 100644 src/automate.test.c

diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 0ff92bf2d..cf96cfbc2 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -188,6 +188,7 @@ set(TESTS_SRC
   tests.c
   academy.test.c
   alchemy.test.c
+  automate.test.c
   battle.test.c
   creport.test.c
   direction.test.c
diff --git a/src/automate.c b/src/automate.c
index 0855ff0e3..c1d4b384b 100644
--- a/src/automate.c
+++ b/src/automate.c
@@ -13,14 +13,6 @@
 
 #include <stdlib.h>
 
-typedef struct student {
-    unit *u;
-    skill_t sk;
-    int level;
-} student;
-
-#define MAXSTUDENTS 128
-
 int cmp_students(const void *lhs, const void *rhs) {
     const student *a = (const student *)lhs;
     const student *b = (const student *)rhs;
@@ -32,16 +24,16 @@ int cmp_students(const void *lhs, const void *rhs) {
     return (int)a->sk - (int)b->sk;
 }
 
-void do_autostudy(region *r) {
+int autostudy_init(student students[], int max_students, region *r)
+{
     unit *u;
     int nstudents = 0;
-    student students[MAXSTUDENTS];
 
     for (u = r->units; u; u = u->next) {
         keyword_t kwd = getkeyword(u->thisorder);
         if (kwd == K_AUTOSTUDY) {
             student * st = students + nstudents;
-            if (++nstudents == MAXSTUDENTS) {
+            if (++nstudents == max_students) {
                 log_fatal("you must increase MAXSTUDENTS");
             }
             st->u = u;
@@ -50,10 +42,18 @@ void do_autostudy(region *r) {
             st->level = effskill_study(u, st->sk);
         }
     }
+    return nstudents;
+}
+
+#define MAXSTUDENTS 128
+
+void do_autostudy(region *r) {
+    student students[MAXSTUDENTS];
+    int nstudents = autostudy_init(students, MAXSTUDENTS, r);
+
     if (nstudents > 0) {
-        int i, taught = 0;
+        int i;
         skill_t sk = NOSKILL;
-        student *teacher = NULL, *student = NULL;
 
         qsort(students, nstudents, sizeof(student), cmp_students);
         for (i = 0; i != nstudents; ++i) {
diff --git a/src/automate.h b/src/automate.h
index ded8f938b..49b052f56 100644
--- a/src/automate.h
+++ b/src/automate.h
@@ -21,8 +21,20 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 #ifndef H_GC_AUTOMATE
 #define H_GC_AUTOMATE
 
+#include "skill.h"
+
 struct region;
+struct unit;
+
+typedef struct student {
+    struct unit *u;
+    skill_t sk;
+    int level;
+    int learn;
+} student;
 
 void do_autostudy(struct region *r);
 
+int autostudy_init(student students[], int max_students, struct region *r);
+
 #endif
diff --git a/src/automate.test.c b/src/automate.test.c
new file mode 100644
index 000000000..d8dc8ed74
--- /dev/null
+++ b/src/automate.test.c
@@ -0,0 +1,38 @@
+#ifdef _MSC_VER
+#include <platform.h>
+#endif
+
+#include "automate.h"
+
+#include "kernel/faction.h"
+#include "kernel/order.h"
+#include "kernel/region.h"
+#include "kernel/unit.h"
+
+#include "tests.h"
+
+#include <CuTest.h>
+
+static void test_autostudy(CuTest *tc) {
+    student students[4];
+    unit *u1, *u2;
+    faction *f;
+    region *r;
+
+    test_setup();
+    r = test_create_plain(0, 0);
+    f = test_create_faction(NULL);
+    u1 = test_create_unit(f, r);
+    u1->thisorder = create_order(K_AUTOSTUDY, f->locale, skillnames[SK_ENTERTAINMENT]);
+    u2 = test_create_unit(f, r);
+    u2->thisorder = create_order(K_AUTOSTUDY, f->locale, skillnames[SK_ENTERTAINMENT]);
+    CuAssertIntEquals(tc, 2, autostudy_init(students, 4, r));
+    test_teardown();
+}
+
+CuSuite *get_automate_suite(void)
+{
+    CuSuite *suite = CuSuiteNew();
+    SUITE_ADD_TEST(suite, test_autostudy);
+    return suite;
+}
diff --git a/src/kernel/order.c b/src/kernel/order.c
index ac2573758..342f45577 100644
--- a/src/kernel/order.c
+++ b/src/kernel/order.c
@@ -84,7 +84,7 @@ char* get_command(const order *ord, const struct locale *lang, char *sbuffer, si
         sbs_strcat(&sbs, str);
         if (ord->id < 0) {
             skill_t sk = (skill_t)(100+ord->id);
-            assert(kwd == K_STUDY && sk != SK_MAGIC && sk < MAXSKILLS);
+            assert((kwd == K_STUDY || kwd == K_AUTOSTUDY) && sk != SK_MAGIC && sk < MAXSKILLS);
             str = skillname(sk, lang);
             if (str) {
                 if (strchr(str, ' ') == NULL) {
diff --git a/src/kernel/order.test.c b/src/kernel/order.test.c
index 3a2f44c56..d1f8b4176 100644
--- a/src/kernel/order.test.c
+++ b/src/kernel/order.test.c
@@ -113,6 +113,30 @@ static void test_parse_make(CuTest *tc) {
     test_teardown();
 }
 
+static void test_parse_autostudy(CuTest *tc) {
+    char cmd[32];
+    order *ord;
+    struct locale * lang;
+
+    test_setup();
+    lang = get_or_create_locale("en");
+    locale_setstring(lang, mkname("skill", skillnames[SK_ENTERTAINMENT]), "Entertainment");
+    locale_setstring(lang, keyword(K_STUDY), "STUDY");
+    locale_setstring(lang, keyword(K_AUTOSTUDY), "AUTOSTUDY");
+    locale_setstring(lang, parameters[P_AUTO], "AUTO");
+    init_locale(lang);
+
+    ord = parse_order("STUDY AUTO Entertainment", lang);
+    CuAssertPtrNotNull(tc, ord);
+    CuAssertIntEquals(tc, K_AUTOSTUDY, getkeyword(ord));
+    CuAssertStrEquals(tc, "AUTOSTUDY Entertainment", get_command(ord, lang, cmd, sizeof(cmd)));
+
+    CuAssertIntEquals(tc, K_AUTOSTUDY, init_order(ord, lang));
+    CuAssertStrEquals(tc, "Entertainment", getstrtoken());
+    free_order(ord);
+    test_teardown();
+}
+
 static void test_parse_make_temp(CuTest *tc) {
     char cmd[32];
     order *ord;
@@ -130,7 +154,7 @@ static void test_parse_make_temp(CuTest *tc) {
     CuAssertIntEquals(tc, K_MAKETEMP, getkeyword(ord));
     CuAssertStrEquals(tc, "MAKETEMP herp", get_command(ord, lang, cmd, sizeof(cmd)));
 
-    CuAssertIntEquals(tc, K_MAKETEMP, init_order_depr(ord));
+    CuAssertIntEquals(tc, K_MAKETEMP, init_order(ord, lang));
     CuAssertStrEquals(tc, "herp", getstrtoken());
     free_order(ord);
     test_teardown();
@@ -153,7 +177,7 @@ static void test_parse_maketemp(CuTest *tc) {
     CuAssertPtrNotNull(tc, ord);
     CuAssertStrEquals(tc, "MAKETEMP herp", get_command(ord, lang, cmd, sizeof(cmd)));
     CuAssertIntEquals(tc, K_MAKETEMP, getkeyword(ord));
-    CuAssertIntEquals(tc, K_MAKETEMP, init_order_depr(ord));
+    CuAssertIntEquals(tc, K_MAKETEMP, init_order(ord, lang));
     CuAssertStrEquals(tc, "herp", getstrtoken());
     free_order(ord);
     test_teardown();
@@ -167,7 +191,7 @@ static void test_init_order(CuTest *tc) {
 
     lang = get_or_create_locale("en");
     ord = create_order(K_MAKETEMP, lang, "hurr durr");
-    CuAssertIntEquals(tc, K_MAKETEMP, init_order_depr(ord));
+    CuAssertIntEquals(tc, K_MAKETEMP, init_order(ord, lang));
     CuAssertStrEquals(tc, "hurr", getstrtoken());
     CuAssertStrEquals(tc, "durr", getstrtoken());
     free_order(ord);
@@ -548,6 +572,7 @@ CuSuite *get_order_suite(void)
     SUITE_ADD_TEST(suite, test_study_order_quoted);
     SUITE_ADD_TEST(suite, test_parse_order);
     SUITE_ADD_TEST(suite, test_parse_make);
+    SUITE_ADD_TEST(suite, test_parse_autostudy);
     SUITE_ADD_TEST(suite, test_parse_make_temp);
     SUITE_ADD_TEST(suite, test_parse_maketemp);
     SUITE_ADD_TEST(suite, test_init_order);
diff --git a/src/study.c b/src/study.c
index 6df5b5eaf..217fe9505 100644
--- a/src/study.c
+++ b/src/study.c
@@ -343,7 +343,7 @@ int teach_cmd(unit * teacher, struct order *ord)
                     init_order(student->thisorder, student->faction->locale);
                     sk = getskill(student->faction->locale);
                     if (sk != NOSKILL
-                        && effskill_study(teacher, sk) - TEACHDIFFERENCE >= effskill(student, sk)) {
+                        && effskill_study(teacher, sk) - TEACHDIFFERENCE >= effskill(student, sk, NULL)) {
                         teaching -= teach_unit(teacher, student, teaching, sk, true, &academy_students);
                     }
                 }
@@ -432,7 +432,7 @@ int teach_cmd(unit * teacher, struct order *ord)
                 continue;
             }
 
-            if (effskill_study(student, sk, NULL) > effskill_study(teacher, sk)
+            if (effskill_study(student, sk) > effskill_study(teacher, sk)
                 - TEACHDIFFERENCE) {
                 if (feedback) {
                     ADDMSG(&teacher->faction->msgs, msg_feedback(teacher, ord, "teach_asgood",
diff --git a/src/test_eressea.c b/src/test_eressea.c
index c299fc09c..5684a8ad4 100644
--- a/src/test_eressea.c
+++ b/src/test_eressea.c
@@ -93,7 +93,6 @@ int RunAllTests(int argc, char *argv[])
     ADD_SUITE(xerewards);
     /* kernel */
     ADD_SUITE(academy);
-    ADD_SUITE(alchemy);
     ADD_SUITE(alliance);
     ADD_SUITE(ally);
     ADD_SUITE(building);
@@ -121,6 +120,8 @@ int RunAllTests(int argc, char *argv[])
     ADD_SUITE(spells);
     ADD_SUITE(unit);
     /* gamecode */
+    ADD_SUITE(alchemy);
+    ADD_SUITE(automate);
     ADD_SUITE(battle);
     ADD_SUITE(calendar);
     ADD_SUITE(creport);

From 9bd439e2e192fbeb97611eaa35a68abece3990b1 Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Sat, 7 Jul 2018 20:56:35 +0200
Subject: [PATCH 23/62] Test, sorting units by skill and level

---
 src/automate.c      |  5 ++++-
 src/automate.test.c | 22 ++++++++++++++++++++--
 2 files changed, 24 insertions(+), 3 deletions(-)

diff --git a/src/automate.c b/src/automate.c
index c1d4b384b..f195bb6db 100644
--- a/src/automate.c
+++ b/src/automate.c
@@ -40,8 +40,12 @@ int autostudy_init(student students[], int max_students, region *r)
             init_order(u->thisorder, u->faction->locale);
             st->sk = getskill(u->faction->locale);
             st->level = effskill_study(u, st->sk);
+            st->learn = 0;
         }
     }
+    if (nstudents > 0) {
+        qsort(students, nstudents, sizeof(student), cmp_students);
+    }
     return nstudents;
 }
 
@@ -55,7 +59,6 @@ void do_autostudy(region *r) {
         int i;
         skill_t sk = NOSKILL;
 
-        qsort(students, nstudents, sizeof(student), cmp_students);
         for (i = 0; i != nstudents; ++i) {
             if (students[i].u) {
                 if (sk == NOSKILL) {
diff --git a/src/automate.test.c b/src/automate.test.c
index d8dc8ed74..3ea640cdf 100644
--- a/src/automate.test.c
+++ b/src/automate.test.c
@@ -15,7 +15,7 @@
 
 static void test_autostudy(CuTest *tc) {
     student students[4];
-    unit *u1, *u2;
+    unit *u1, *u2, *u3;
     faction *f;
     region *r;
 
@@ -24,9 +24,27 @@ static void test_autostudy(CuTest *tc) {
     f = test_create_faction(NULL);
     u1 = test_create_unit(f, r);
     u1->thisorder = create_order(K_AUTOSTUDY, f->locale, skillnames[SK_ENTERTAINMENT]);
+    test_create_unit(f, r);
     u2 = test_create_unit(f, r);
     u2->thisorder = create_order(K_AUTOSTUDY, f->locale, skillnames[SK_ENTERTAINMENT]);
-    CuAssertIntEquals(tc, 2, autostudy_init(students, 4, r));
+    set_level(u2, SK_ENTERTAINMENT, 2);
+    u3 = test_create_unit(f, r);
+    u3->thisorder = create_order(K_AUTOSTUDY, f->locale, skillnames[SK_PERCEPTION]);
+    students[3].u = NULL;
+    CuAssertIntEquals(tc, 3, autostudy_init(students, 4, r));
+    CuAssertPtrEquals(tc, u2, students[0].u);
+    CuAssertIntEquals(tc, 2, students[0].level);
+    CuAssertIntEquals(tc, 0, students[0].learn);
+    CuAssertIntEquals(tc, SK_ENTERTAINMENT, students[0].sk);
+    CuAssertPtrEquals(tc, u1, students[1].u);
+    CuAssertIntEquals(tc, 0, students[1].level);
+    CuAssertIntEquals(tc, 0, students[1].learn);
+    CuAssertIntEquals(tc, SK_ENTERTAINMENT, students[1].sk);
+    CuAssertPtrEquals(tc, u3, students[2].u);
+    CuAssertIntEquals(tc, 0, students[2].level);
+    CuAssertIntEquals(tc, 0, students[2].learn);
+    CuAssertIntEquals(tc, SK_PERCEPTION, students[2].sk);
+    CuAssertPtrEquals(tc, NULL, students[3].u);
     test_teardown();
 }
 

From 7affbd6c74a9ebebf8d949a2b881e607a423a4ea Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Mon, 9 Jul 2018 03:31:13 +0200
Subject: [PATCH 24/62] autostudy continued (WIP)

---
 src/academy.c       |   8 ++--
 src/academy.h       |   2 +-
 src/automate.c      | 101 +++++++++++++++++++++++++++++++-------------
 src/automate.h      |   7 +--
 src/automate.test.c |  64 ++++++++++++++++++++--------
 src/study.c         |  86 ++++++++++++++++++-------------------
 6 files changed, 170 insertions(+), 98 deletions(-)

diff --git a/src/academy.c b/src/academy.c
index 5df13efa4..1298e9b57 100644
--- a/src/academy.c
+++ b/src/academy.c
@@ -33,13 +33,13 @@ void academy_teaching_bonus(struct unit *u, skill_t sk, int students) {
     }
 }
 
-bool academy_can_teach(unit *teacher, unit *student, skill_t sk) {
+bool academy_can_teach(unit *teacher, unit *scholar, skill_t sk) {
     const struct building_type *btype = bt_find("academy");
-    if (active_building(teacher, btype) && active_building(student, btype)) {
-        int j = study_cost(student, sk) * 2;
+    if (active_building(teacher, btype) && active_building(scholar, btype)) {
+        int j = study_cost(scholar, sk) * 2;
         if (j < 50) j = 50;
         /* kann Einheit das zahlen? */
-        return get_pooled(student, get_resourcetype(R_SILVER), GET_DEFAULT, j) >= j;
+        return get_pooled(scholar, get_resourcetype(R_SILVER), GET_DEFAULT, j) >= j;
         /* sonst nehmen sie nicht am Unterricht teil */
     }
     return false;
diff --git a/src/academy.h b/src/academy.h
index f6af93748..3c496d8fa 100644
--- a/src/academy.h
+++ b/src/academy.h
@@ -9,7 +9,7 @@ extern "C" {
 
     struct unit;
     void academy_teaching_bonus(struct unit *u, skill_t sk, int academy);
-    bool academy_can_teach(struct unit *teacher, struct unit *student, skill_t sk);
+    bool academy_can_teach(struct unit *teacher, struct unit *scholar, skill_t sk);
 #ifdef __cplusplus
 }
 #endif
diff --git a/src/automate.c b/src/automate.c
index f195bb6db..997f67bd8 100644
--- a/src/automate.c
+++ b/src/automate.c
@@ -13,9 +13,10 @@
 
 #include <stdlib.h>
 
-int cmp_students(const void *lhs, const void *rhs) {
-    const student *a = (const student *)lhs;
-    const student *b = (const student *)rhs;
+int cmp_scholars(const void *lhs, const void *rhs)
+{
+    const scholar *a = (const scholar *)lhs;
+    const scholar *b = (const scholar *)rhs;
     if (a->sk == b->sk) {
         /* sort by level, descending: */
         return b->level - a->level;
@@ -24,17 +25,17 @@ int cmp_students(const void *lhs, const void *rhs) {
     return (int)a->sk - (int)b->sk;
 }
 
-int autostudy_init(student students[], int max_students, region *r)
+int autostudy_init(scholar scholars[], int max_scholars, region *r)
 {
     unit *u;
-    int nstudents = 0;
+    int nscholars = 0;
 
     for (u = r->units; u; u = u->next) {
         keyword_t kwd = getkeyword(u->thisorder);
         if (kwd == K_AUTOSTUDY) {
-            student * st = students + nstudents;
-            if (++nstudents == max_students) {
-                log_fatal("you must increase MAXSTUDENTS");
+            scholar * st = scholars + nscholars;
+            if (++nscholars == max_scholars) {
+                log_fatal("you must increase MAXSCHOLARS");
             }
             st->u = u;
             init_order(u->thisorder, u->faction->locale);
@@ -43,31 +44,73 @@ int autostudy_init(student students[], int max_students, region *r)
             st->learn = 0;
         }
     }
-    if (nstudents > 0) {
-        qsort(students, nstudents, sizeof(student), cmp_students);
+    if (nscholars > 0) {
+        qsort(scholars, nscholars, sizeof(scholar), cmp_scholars);
     }
-    return nstudents;
+    return nscholars;
 }
 
-#define MAXSTUDENTS 128
-
-void do_autostudy(region *r) {
-    student students[MAXSTUDENTS];
-    int nstudents = autostudy_init(students, MAXSTUDENTS, r);
-
-    if (nstudents > 0) {
-        int i;
-        skill_t sk = NOSKILL;
-
-        for (i = 0; i != nstudents; ++i) {
-            if (students[i].u) {
-                if (sk == NOSKILL) {
-                    sk = students[i].sk;
+void autostudy_run(scholar scholars[], int nscholars)
+{
+    int i, t, s, ti = 0, si = 0, ts = 0, tt = 0;
+    skill_t sk = scholars[0].sk;
+    for (i = ti; i != nscholars && scholars[i].sk == sk; ++i) {
+        int mint;
+        ts += scholars[i].u->number; /* count total scholars */
+        mint = (ts + 10) / 11; /* need a minimum of ceil(ts/11) teachers */
+        while (mint>tt) {
+            tt += scholars[si++].u->number;
+        }
+    }
+    /* now si splits the teachers and students 1:10 */
+    /* first student must be 2 levels below first teacher: */
+    while (scholars[ti].level - TEACHDIFFERENCE > scholars[si].level) {
+        tt += scholars[si++].u->number;
+    }
+    /* invariant: unit ti can still teach i students */
+    i = scholars[ti].u->number * 10;
+    for (t = ti, s = si; t != si && s != nscholars; ++t) {
+        /* TODO: is there no constant for students per teacher? */
+        while (s != nscholars) {
+            int n = scholars[s].u->number;
+            scholars[s].learn += n;
+            if (i >= n) {
+                i -= n;
+                scholars[s].learn += n;
+                /* next student */
+            }
+            else {
+                scholars[s].learn += i;
+                /* go to next suitable teacher */
+                do {
+                    ++t;
                 }
-                else if (sk != students[i].sk) {
-                    continue;
-                }
-                students[i].u = NULL;
+                while (scholars[t].level - TEACHDIFFERENCE < scholars[s].level);
+            }
+        }
+    }
+}
+
+#define MAXSCHOLARS 128
+
+void do_autostudy(region *r)
+{
+    scholar scholars[MAXSCHOLARS];
+    int nscholars = autostudy_init(scholars, MAXSCHOLARS, r);
+
+    if (nscholars > 0) {
+        int i;
+        skill_t sk = NOSKILL;
+
+        for (i = 0; i != nscholars; ++i) {
+            if (scholars[i].u) {
+                if (sk == NOSKILL) {
+                    sk = scholars[i].sk;
+                }
+                else if (sk != scholars[i].sk) {
+                    continue;
+                }
+                scholars[i].u = NULL;
             }
         }
     }
diff --git a/src/automate.h b/src/automate.h
index 49b052f56..285fc6638 100644
--- a/src/automate.h
+++ b/src/automate.h
@@ -26,15 +26,16 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 struct region;
 struct unit;
 
-typedef struct student {
+typedef struct scholar {
     struct unit *u;
     skill_t sk;
     int level;
     int learn;
-} student;
+} scholar;
 
 void do_autostudy(struct region *r);
 
-int autostudy_init(student students[], int max_students, struct region *r);
+int autostudy_init(scholar scholars[], int max_scholars, struct region *r);
+void autostudy_run(scholar scholars[], int nscholars);
 
 #endif
diff --git a/src/automate.test.c b/src/automate.test.c
index 3ea640cdf..7404a2d9c 100644
--- a/src/automate.test.c
+++ b/src/automate.test.c
@@ -13,8 +13,8 @@
 
 #include <CuTest.h>
 
-static void test_autostudy(CuTest *tc) {
-    student students[4];
+static void test_autostudy_init(CuTest *tc) {
+    scholar scholars[4];
     unit *u1, *u2, *u3;
     faction *f;
     region *r;
@@ -30,27 +30,55 @@ static void test_autostudy(CuTest *tc) {
     set_level(u2, SK_ENTERTAINMENT, 2);
     u3 = test_create_unit(f, r);
     u3->thisorder = create_order(K_AUTOSTUDY, f->locale, skillnames[SK_PERCEPTION]);
-    students[3].u = NULL;
-    CuAssertIntEquals(tc, 3, autostudy_init(students, 4, r));
-    CuAssertPtrEquals(tc, u2, students[0].u);
-    CuAssertIntEquals(tc, 2, students[0].level);
-    CuAssertIntEquals(tc, 0, students[0].learn);
-    CuAssertIntEquals(tc, SK_ENTERTAINMENT, students[0].sk);
-    CuAssertPtrEquals(tc, u1, students[1].u);
-    CuAssertIntEquals(tc, 0, students[1].level);
-    CuAssertIntEquals(tc, 0, students[1].learn);
-    CuAssertIntEquals(tc, SK_ENTERTAINMENT, students[1].sk);
-    CuAssertPtrEquals(tc, u3, students[2].u);
-    CuAssertIntEquals(tc, 0, students[2].level);
-    CuAssertIntEquals(tc, 0, students[2].learn);
-    CuAssertIntEquals(tc, SK_PERCEPTION, students[2].sk);
-    CuAssertPtrEquals(tc, NULL, students[3].u);
+    scholars[3].u = NULL;
+    CuAssertIntEquals(tc, 3, autostudy_init(scholars, 4, r));
+    CuAssertPtrEquals(tc, u2, scholars[0].u);
+    CuAssertIntEquals(tc, 2, scholars[0].level);
+    CuAssertIntEquals(tc, 0, scholars[0].learn);
+    CuAssertIntEquals(tc, SK_ENTERTAINMENT, scholars[0].sk);
+    CuAssertPtrEquals(tc, u1, scholars[1].u);
+    CuAssertIntEquals(tc, 0, scholars[1].level);
+    CuAssertIntEquals(tc, 0, scholars[1].learn);
+    CuAssertIntEquals(tc, SK_ENTERTAINMENT, scholars[1].sk);
+    CuAssertPtrEquals(tc, u3, scholars[2].u);
+    CuAssertIntEquals(tc, 0, scholars[2].level);
+    CuAssertIntEquals(tc, 0, scholars[2].learn);
+    CuAssertIntEquals(tc, SK_PERCEPTION, scholars[2].sk);
+    CuAssertPtrEquals(tc, NULL, scholars[3].u);
     test_teardown();
 }
 
+static void test_autostudy_run(CuTest *tc) {
+    scholar scholars[4];
+    unit *u1, *u2, *u3;
+    faction *f;
+    region *r;
+
+    test_setup();
+    r = test_create_plain(0, 0);
+    f = test_create_faction(NULL);
+    u1 = test_create_unit(f, r);
+    u1->thisorder = create_order(K_AUTOSTUDY, f->locale, skillnames[SK_ENTERTAINMENT]);
+    set_number(u1, 2);
+    u2 = test_create_unit(f, r);
+    u2->thisorder = create_order(K_AUTOSTUDY, f->locale, skillnames[SK_ENTERTAINMENT]);
+    set_number(u2, 10);
+    set_level(u2, SK_ENTERTAINMENT, 2);
+    u3 = test_create_unit(f, r);
+    u3->thisorder = create_order(K_AUTOSTUDY, f->locale, skillnames[SK_PERCEPTION]);
+    set_number(u3, 20);
+    scholars[3].u = NULL;
+    CuAssertIntEquals(tc, 3, autostudy_init(scholars, 4, r));
+    autostudy_run(scholars, 3);
+    CuAssertIntEquals(tc, 0, scholars[0].learn);
+    CuAssertIntEquals(tc, 20, scholars[1].learn);
+    CuAssertIntEquals(tc, 30, scholars[2].learn);
+}
+
 CuSuite *get_automate_suite(void)
 {
     CuSuite *suite = CuSuiteNew();
-    SUITE_ADD_TEST(suite, test_autostudy);
+    SUITE_ADD_TEST(suite, test_autostudy_init);
+    SUITE_ADD_TEST(suite, test_autostudy_run);
     return suite;
 }
diff --git a/src/study.c b/src/study.c
index 217fe9505..0dc4386bc 100644
--- a/src/study.c
+++ b/src/study.c
@@ -195,37 +195,37 @@ const attrib_type at_learning = {
 
 #define EXPERIENCEDAYS 10
 
-static int study_days(unit * student, skill_t sk)
+static int study_days(unit * scholar, skill_t sk)
 {
     int speed = STUDYDAYS;
-    if (u_race(student)->study_speed) {
-        speed += u_race(student)->study_speed[sk];
+    if (u_race(scholar)->study_speed) {
+        speed += u_race(scholar)->study_speed[sk];
         if (speed < STUDYDAYS) {
-            skill *sv = unit_skill(student, sk);
+            skill *sv = unit_skill(scholar, sk);
             if (sv == 0) {
                 speed = STUDYDAYS;
             }
         }
     }
-    return student->number * speed;
+    return scholar->number * speed;
 }
 
 static int
-teach_unit(unit * teacher, unit * student, int nteaching, skill_t sk,
+teach_unit(unit * teacher, unit * scholar, int nteaching, skill_t sk,
     bool report, int *academy_students)
 {
     teaching_info *teach = NULL;
     attrib *a;
     int students;
 
-    if (magic_lowskill(student)) {
+    if (magic_lowskill(scholar)) {
         cmistake(teacher, teacher->thisorder, 292, MSG_EVENT);
         return 0;
     }
 
-    students = student->number;
+    students = scholar->number;
     /* subtract already taught students */
-    a = a_find(student->attribs, &at_learning);
+    a = a_find(scholar->attribs, &at_learning);
     if (a != NULL) {
         teach = (teaching_info *)a->data.v;
         students -= teach->students;
@@ -235,18 +235,18 @@ teach_unit(unit * teacher, unit * student, int nteaching, skill_t sk,
 
     if (students > 0) {
         if (teach == NULL) {
-            a = a_add(&student->attribs, a_new(&at_learning));
+            a = a_add(&scholar->attribs, a_new(&at_learning));
             teach = (teaching_info *)a->data.v;
         }
         selist_push(&teach->teachers, teacher);
         teach->days += students * STUDYDAYS;
         teach->students += students; 
 
-        if (student->building && teacher->building == student->building) {
+        if (scholar->building && teacher->building == scholar->building) {
             /* Solange Akademien groessenbeschraenkt sind, sollte Lehrer und
              * Student auch in unterschiedlichen Gebaeuden stehen duerfen */
             /* FIXME comment contradicts implementation */
-            if (academy_can_teach(teacher, student, sk)) {
+            if (academy_can_teach(teacher, scholar, sk)) {
                 /* Jeder Schueler zusaetzlich +10 Tage wenn in Uni. */
                 teach->days += students * EXPERIENCEDAYS;  /* learning erhoehen */
                 /* Lehrer zusaetzlich +1 Tag pro Schueler. */
@@ -304,7 +304,7 @@ int teach_cmd(unit * teacher, struct order *ord)
 #if TEACH_ALL
     if (getparam(teacher->faction->locale) == P_ANY) {
         skill_t sk;
-        unit *student;
+        unit *scholar;
         skill_t teachskill[MAXSKILLS];
         int t = 0;
 
@@ -313,15 +313,15 @@ int teach_cmd(unit * teacher, struct order *ord)
             teachskill[t] = getskill(teacher->faction->locale);
         } while (sk != NOSKILL);
 
-        for (student = r->units; teaching > 0 && student; student = student->next) {
-            if (LongHunger(student)) {
+        for (scholar = r->units; teaching > 0 && scholar; scholar = scholar->next) {
+            if (LongHunger(scholar)) {
                 continue;
             }
-            else if (student->faction == teacher->faction) {
-                if (getkeyword(student->thisorder) == K_STUDY) {
+            else if (scholar->faction == teacher->faction) {
+                if (getkeyword(scholar->thisorder) == K_STUDY) {
                     /* Input ist nun von student->thisorder !! */
-                    init_order(student->thisorder, student->faction->locale);
-                    sk = getskill(student->faction->locale);
+                    init_order(scholar->thisorder, scholar->faction->locale);
+                    sk = getskill(scholar->faction->locale);
                     if (sk != NOSKILL && teachskill[0] != NOSKILL) {
                         for (t = 0; teachskill[t] != NOSKILL; ++t) {
                             if (sk == teachskill[t]) {
@@ -331,20 +331,20 @@ int teach_cmd(unit * teacher, struct order *ord)
                         sk = teachskill[t];
                     }
                     if (sk != NOSKILL
-                        && effskill_study(teacher, sk) - TEACHDIFFERENCE > effskill_study(student, sk)) {
-                        teaching -= teach_unit(teacher, student, teaching, sk, true, &academy_students);
+                        && effskill_study(teacher, sk) - TEACHDIFFERENCE > effskill_study(scholar, sk)) {
+                        teaching -= teach_unit(teacher, scholar, teaching, sk, true, &academy_students);
                     }
                 }
             }
 #ifdef TEACH_FRIENDS
-            else if (alliedunit(teacher, student->faction, HELP_GUARD)) {
-                if (getkeyword(student->thisorder) == K_STUDY) {
+            else if (alliedunit(teacher, scholar->faction, HELP_GUARD)) {
+                if (getkeyword(scholar->thisorder) == K_STUDY) {
                     /* Input ist nun von student->thisorder !! */
-                    init_order(student->thisorder, student->faction->locale);
-                    sk = getskill(student->faction->locale);
+                    init_order(scholar->thisorder, scholar->faction->locale);
+                    sk = getskill(scholar->faction->locale);
                     if (sk != NOSKILL
-                        && effskill_study(teacher, sk) - TEACHDIFFERENCE >= effskill(student, sk, NULL)) {
-                        teaching -= teach_unit(teacher, student, teaching, sk, true, &academy_students);
+                        && effskill_study(teacher, sk) - TEACHDIFFERENCE >= effskill(scholar, sk, NULL)) {
+                        teaching -= teach_unit(teacher, scholar, teaching, sk, true, &academy_students);
                     }
                 }
             }
@@ -363,15 +363,15 @@ int teach_cmd(unit * teacher, struct order *ord)
 
         while (!parser_end()) {
             skill_t sk;
-            unit *student;
+            unit *scholar;
             bool feedback;
 
-            getunit(r, teacher->faction, &student);
+            getunit(r, teacher->faction, &scholar);
             ++count;
 
             /* Falls die Unit nicht gefunden wird, Fehler melden */
 
-            if (!student) {
+            if (!scholar) {
                 char tbuf[20];
                 const char *uid;
                 const char *token;
@@ -403,8 +403,8 @@ int teach_cmd(unit * teacher, struct order *ord)
                 continue;
             }
 
-            feedback = teacher->faction == student->faction
-                || alliedunit(student, teacher->faction, HELP_GUARD);
+            feedback = teacher->faction == scholar->faction
+                || alliedunit(scholar, teacher->faction, HELP_GUARD);
 
             /* Neuen Befehl zusammenbauen. TEMP-Einheiten werden automatisch in
              * ihre neuen Nummern uebersetzt. */
@@ -412,31 +412,31 @@ int teach_cmd(unit * teacher, struct order *ord)
                 strncat(zOrder, " ", sz - 1);
                 --sz;
             }
-            sz -= str_strlcpy(zOrder + 4096 - sz, itoa36(student->no), sz);
+            sz -= str_strlcpy(zOrder + 4096 - sz, itoa36(scholar->no), sz);
 
-            if (getkeyword(student->thisorder) != K_STUDY) {
+            if (getkeyword(scholar->thisorder) != K_STUDY) {
                 ADDMSG(&teacher->faction->msgs,
-                    msg_feedback(teacher, ord, "teach_nolearn", "student", student));
+                    msg_feedback(teacher, ord, "teach_nolearn", "student", scholar));
                 continue;
             }
 
             /* Input ist nun von student->thisorder !! */
             parser_pushstate();
-            init_order(student->thisorder, student->faction->locale);
-            sk = getskill(student->faction->locale);
+            init_order(scholar->thisorder, scholar->faction->locale);
+            sk = getskill(scholar->faction->locale);
             parser_popstate();
 
             if (sk == NOSKILL) {
                 ADDMSG(&teacher->faction->msgs,
-                    msg_feedback(teacher, ord, "teach_nolearn", "student", student));
+                    msg_feedback(teacher, ord, "teach_nolearn", "student", scholar));
                 continue;
             }
 
-            if (effskill_study(student, sk) > effskill_study(teacher, sk)
+            if (effskill_study(scholar, sk) > effskill_study(teacher, sk)
                 - TEACHDIFFERENCE) {
                 if (feedback) {
                     ADDMSG(&teacher->faction->msgs, msg_feedback(teacher, ord, "teach_asgood",
-                        "student", student));
+                        "student", scholar));
                 }
                 continue;
             }
@@ -444,18 +444,18 @@ int teach_cmd(unit * teacher, struct order *ord)
                 /* ist der Magier schon spezialisiert, so versteht er nur noch
                  * Lehrer seines Gebietes */
                 sc_mage *mage1 = get_mage_depr(teacher);
-                sc_mage *mage2 = get_mage_depr(student);
+                sc_mage *mage2 = get_mage_depr(scholar);
                 if (mage2 && mage1 && mage2->magietyp != M_GRAY
                     && mage1->magietyp != mage2->magietyp) {
                     if (feedback) {
                         ADDMSG(&teacher->faction->msgs, msg_feedback(teacher, ord,
-                            "error_different_magic", "target", student));
+                            "error_different_magic", "target", scholar));
                     }
                     continue;
                 }
             }
             sk_academy = sk;
-            teaching -= teach_unit(teacher, student, teaching, sk, false, &academy_students);
+            teaching -= teach_unit(teacher, scholar, teaching, sk, false, &academy_students);
         }
         new_order = create_order(K_TEACH, teacher->faction->locale, "%s", zOrder);
         replace_order(&teacher->orders, ord, new_order);

From 51740476baa501fcc17803bfe15f7350ce522571 Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Wed, 11 Jul 2018 21:03:00 +0200
Subject: [PATCH 25/62] finished autostudy

---
 src/automate.c      | 110 +++++++++++++++++++++++++++++++-------------
 src/automate.h      |   2 +
 src/automate.test.c |  34 ++++++++++++--
 3 files changed, 111 insertions(+), 35 deletions(-)

diff --git a/src/automate.c b/src/automate.c
index 997f67bd8..bf130acf3 100644
--- a/src/automate.c
+++ b/src/automate.c
@@ -12,6 +12,7 @@
 #include "study.h"
 
 #include <stdlib.h>
+#include <assert.h>
 
 int cmp_scholars(const void *lhs, const void *rhs)
 {
@@ -50,44 +51,89 @@ int autostudy_init(scholar scholars[], int max_scholars, region *r)
     return nscholars;
 }
 
+static void teaching(scholar *s, int n) {
+    assert(n <= s->u->number);
+    s->learn += n;
+}
+
+static void learning(scholar *s, int n) {
+    assert(n <= s->u->number);
+    s->learn += n;
+}
+
 void autostudy_run(scholar scholars[], int nscholars)
 {
-    int i, t, s, ti = 0, si = 0, ts = 0, tt = 0;
-    skill_t sk = scholars[0].sk;
-    for (i = ti; i != nscholars && scholars[i].sk == sk; ++i) {
-        int mint;
-        ts += scholars[i].u->number; /* count total scholars */
-        mint = (ts + 10) / 11; /* need a minimum of ceil(ts/11) teachers */
-        while (mint>tt) {
-            tt += scholars[si++].u->number;
-        }
-    }
-    /* now si splits the teachers and students 1:10 */
-    /* first student must be 2 levels below first teacher: */
-    while (scholars[ti].level - TEACHDIFFERENCE > scholars[si].level) {
-        tt += scholars[si++].u->number;
-    }
-    /* invariant: unit ti can still teach i students */
-    i = scholars[ti].u->number * 10;
-    for (t = ti, s = si; t != si && s != nscholars; ++t) {
-        /* TODO: is there no constant for students per teacher? */
-        while (s != nscholars) {
-            int n = scholars[s].u->number;
-            scholars[s].learn += n;
-            if (i >= n) {
-                i -= n;
-                scholars[s].learn += n;
-                /* next student */
+    int ti = 0;
+    while (ti != nscholars) {
+        skill_t sk = scholars[ti].sk;
+        int t, s, se, ts = 0, tt = 0, si = ti;
+        for (se = ti; se != nscholars && scholars[se].sk == sk; ++se) {
+            int mint;
+            ts += scholars[se].u->number; /* count total scholars */
+            mint = (ts + 10) / 11; /* need a minimum of ceil(ts/11) teachers */
+            for (; mint > tt && si != nscholars; ++si) {
+                tt += scholars[si].u->number;
             }
-            else {
-                scholars[s].learn += i;
-                /* go to next suitable teacher */
-                do {
-                    ++t;
+        }
+        /* now si splits the teachers and students 1:10 */
+        /* first student must be 2 levels below first teacher: */
+        for (; si != se && scholars[ti].level - TEACHDIFFERENCE > scholars[si].level; ++si) {
+            tt += scholars[si].u->number;
+        }
+        if (si == se) {
+            /* there are no students, so standard learning only */
+            for (t = ti; t != se; ++t) {
+                learning(scholars + t, scholars[t].u->number);
+            }
+        }
+        else {
+            /* invariant: unit ti can still teach i students */
+            int i = scholars[ti].u->number * STUDENTS_PER_TEACHER;
+            /* invariant: unit si has n students that can still be taught */
+            int n = scholars[si].u->number;
+            for (t = ti, s = si; t != si && s != se; ) {
+                if (i > n) {
+                    /* t has more than enough teaching capacity for s */
+                    i -= n;
+                    teaching(scholars + s, n);
+                    learning(scholars + s, scholars[s].u->number);
+                    /* next student, please: */
+                    if (++s == se) {
+                        continue;
+                    }
+                    n = scholars[s].u->number;
+                }
+                else {
+                    /* s gets partial credit and we need a new teacher */
+                    teaching(scholars + s, i);
+
+                    /* we are done with this teacher. any remaining people are regular learners: */
+                    if (scholars[t].u->number > 1) {
+                        /* remain = number - ceil(taught/10); */
+                        int remain = (STUDENTS_PER_TEACHER * scholars[t].u->number - i + STUDENTS_PER_TEACHER - 1) / STUDENTS_PER_TEACHER;
+                        learning(scholars + t, remain);
+                    }
+
+                    /* we want a new teacher for s. if any exists, it's next in the sequence. */
+                    if (++t == si) {
+                        continue;
+                    }
+                    if (scholars[t].level - TEACHDIFFERENCE < scholars[s].level) {
+                        /* next teacher cannot teach, we must skip students. */
+                        do {
+                            learning(scholars + s, (n - i));
+                            i = 0;
+                            if (++s == se) {
+                                continue;
+                            }
+                            n = scholars[s].u->number;
+                        } while (scholars[t].level - TEACHDIFFERENCE < scholars[s].level);
+                    }
+                    i = scholars[t].u->number * STUDENTS_PER_TEACHER;
                 }
-                while (scholars[t].level - TEACHDIFFERENCE < scholars[s].level);
             }
         }
+        ti = se;
     }
 }
 
diff --git a/src/automate.h b/src/automate.h
index 285fc6638..61fed6866 100644
--- a/src/automate.h
+++ b/src/automate.h
@@ -33,6 +33,8 @@ typedef struct scholar {
     int learn;
 } scholar;
 
+#define STUDENTS_PER_TEACHER 10
+
 void do_autostudy(struct region *r);
 
 int autostudy_init(scholar scholars[], int max_scholars, struct region *r);
diff --git a/src/automate.test.c b/src/automate.test.c
index 7404a2d9c..999cb25d4 100644
--- a/src/automate.test.c
+++ b/src/automate.test.c
@@ -60,19 +60,46 @@ static void test_autostudy_run(CuTest *tc) {
     u1 = test_create_unit(f, r);
     u1->thisorder = create_order(K_AUTOSTUDY, f->locale, skillnames[SK_ENTERTAINMENT]);
     set_number(u1, 2);
+    set_level(u1, SK_ENTERTAINMENT, 2);
     u2 = test_create_unit(f, r);
     u2->thisorder = create_order(K_AUTOSTUDY, f->locale, skillnames[SK_ENTERTAINMENT]);
     set_number(u2, 10);
-    set_level(u2, SK_ENTERTAINMENT, 2);
     u3 = test_create_unit(f, r);
     u3->thisorder = create_order(K_AUTOSTUDY, f->locale, skillnames[SK_PERCEPTION]);
-    set_number(u3, 20);
+    set_number(u3, 15);
     scholars[3].u = NULL;
     CuAssertIntEquals(tc, 3, autostudy_init(scholars, 4, r));
     autostudy_run(scholars, 3);
     CuAssertIntEquals(tc, 0, scholars[0].learn);
     CuAssertIntEquals(tc, 20, scholars[1].learn);
-    CuAssertIntEquals(tc, 30, scholars[2].learn);
+    CuAssertIntEquals(tc, 15, scholars[2].learn);
+}
+
+static void test_autostudy_run_noteachers(CuTest *tc) {
+    scholar scholars[4];
+    unit *u1, *u2, *u3;
+    faction *f;
+    region *r;
+
+    test_setup();
+    r = test_create_plain(0, 0);
+    f = test_create_faction(NULL);
+    u1 = test_create_unit(f, r);
+    u1->thisorder = create_order(K_AUTOSTUDY, f->locale, skillnames[SK_LUMBERJACK]);
+    set_number(u1, 2);
+    set_level(u1, SK_ENTERTAINMENT, 2);
+    u2 = test_create_unit(f, r);
+    u2->thisorder = create_order(K_AUTOSTUDY, f->locale, skillnames[SK_ENTERTAINMENT]);
+    set_number(u2, 10);
+    u3 = test_create_unit(f, r);
+    u3->thisorder = create_order(K_AUTOSTUDY, f->locale, skillnames[SK_PERCEPTION]);
+    set_number(u3, 15);
+    scholars[3].u = NULL;
+    CuAssertIntEquals(tc, 3, autostudy_init(scholars, 4, r));
+    autostudy_run(scholars, 3);
+    CuAssertIntEquals(tc, 2, scholars[0].learn);
+    CuAssertIntEquals(tc, 10, scholars[1].learn);
+    CuAssertIntEquals(tc, 15, scholars[2].learn);
 }
 
 CuSuite *get_automate_suite(void)
@@ -80,5 +107,6 @@ CuSuite *get_automate_suite(void)
     CuSuite *suite = CuSuiteNew();
     SUITE_ADD_TEST(suite, test_autostudy_init);
     SUITE_ADD_TEST(suite, test_autostudy_run);
+    SUITE_ADD_TEST(suite, test_autostudy_run_noteachers);
     return suite;
 }

From c4424f9899376c0b6a4d3e844ff830cc0fc48a54 Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Wed, 11 Jul 2018 21:07:31 +0200
Subject: [PATCH 26/62] actually do the learning.

---
 src/automate.c | 22 +++++-----------------
 src/study.c    |  6 +++---
 2 files changed, 8 insertions(+), 20 deletions(-)

diff --git a/src/automate.c b/src/automate.c
index bf130acf3..e08d7a625 100644
--- a/src/automate.c
+++ b/src/automate.c
@@ -142,22 +142,10 @@ void autostudy_run(scholar scholars[], int nscholars)
 void do_autostudy(region *r)
 {
     scholar scholars[MAXSCHOLARS];
-    int nscholars = autostudy_init(scholars, MAXSCHOLARS, r);
-
-    if (nscholars > 0) {
-        int i;
-        skill_t sk = NOSKILL;
-
-        for (i = 0; i != nscholars; ++i) {
-            if (scholars[i].u) {
-                if (sk == NOSKILL) {
-                    sk = scholars[i].sk;
-                }
-                else if (sk != scholars[i].sk) {
-                    continue;
-                }
-                scholars[i].u = NULL;
-            }
-        }
+    int i, nscholars = autostudy_init(scholars, MAXSCHOLARS, r);
+    autostudy_run(scholars, nscholars);
+    for (i = 0; i != nscholars; ++i) {
+        int days = STUDYDAYS * scholars[i].learn;
+        learn_skill(scholars[i].u, scholars[i].sk, days);
     }
 }
diff --git a/src/study.c b/src/study.c
index 0dc4386bc..bd29fc2a8 100644
--- a/src/study.c
+++ b/src/study.c
@@ -766,9 +766,6 @@ int study_cmd(unit * u, order * ord)
         days *= 2;
     }
 
-    if (fval(u, UFL_HUNGER))
-        days /= 2;
-
     learn_skill(u, sk, days);
     if (a != NULL) {
         if (teach->teachers) {
@@ -832,6 +829,9 @@ void learn_skill(unit *u, skill_t sk, int days) {
     int leveldays = STUDYDAYS * u->number;
     int weeks = 0;
 
+    if (fval(u, UFL_HUNGER)) {
+        days /= 2;
+    }
     assert(sk >= 0 && sk < MAXSKILLS);
     if (inject_learn_fun) {
         inject_learn_fun(u, sk, days);

From af0a359acf03250653bdfe5f4d321e3b68e3aedf Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Wed, 11 Jul 2018 21:39:33 +0200
Subject: [PATCH 27/62] make local comparison function static

---
 clibs          | 2 +-
 src/automate.c | 2 +-
 storage        | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/clibs b/clibs
index 9b6e34959..f9842e07a 160000
--- a/clibs
+++ b/clibs
@@ -1 +1 @@
-Subproject commit 9b6e34959f77d7ca3a4ce3826cb487487f557441
+Subproject commit f9842e07a442c5453c270badf25ab72633b4edf5
diff --git a/src/automate.c b/src/automate.c
index e08d7a625..2f188f352 100644
--- a/src/automate.c
+++ b/src/automate.c
@@ -14,7 +14,7 @@
 #include <stdlib.h>
 #include <assert.h>
 
-int cmp_scholars(const void *lhs, const void *rhs)
+static int cmp_scholars(const void *lhs, const void *rhs)
 {
     const scholar *a = (const scholar *)lhs;
     const scholar *b = (const scholar *)rhs;
diff --git a/storage b/storage
index 6eea76952..5623ee652 160000
--- a/storage
+++ b/storage
@@ -1 +1 @@
-Subproject commit 6eea7695285f9c9f1d25421897ca55f466ef7566
+Subproject commit 5623ee6527e97af20c7d8efc03800b6fe1579744

From 25d93f3a182ca592445cb29da1386216237694e1 Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Thu, 12 Jul 2018 20:52:09 +0200
Subject: [PATCH 28/62] update storage library with needed bugfix

---
 storage | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/storage b/storage
index 5623ee652..0ef4b39e3 160000
--- a/storage
+++ b/storage
@@ -1 +1 @@
-Subproject commit 5623ee6527e97af20c7d8efc03800b6fe1579744
+Subproject commit 0ef4b39e39d8146d6ca323500a285c53843db988

From 21cbff0740a827fd7aff8af77c1c772ef79fed98 Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Thu, 12 Jul 2018 20:54:28 +0200
Subject: [PATCH 29/62] update submodules

---
 storage | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/storage b/storage
index 6eea76952..0ef4b39e3 160000
--- a/storage
+++ b/storage
@@ -1 +1 @@
-Subproject commit 6eea7695285f9c9f1d25421897ca55f466ef7566
+Subproject commit 0ef4b39e39d8146d6ca323500a285c53843db988

From 902ce9bf69ff1d8751a243738fb1876788cdfd9b Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Sat, 14 Jul 2018 15:56:44 +0200
Subject: [PATCH 30/62] extract long_order_allowed for use in autostudy.

---
 clibs               |  2 +-
 src/automate.c      | 26 ++++++++++++++++++--------
 src/automate.test.c |  2 ++
 src/laws.c          | 31 ++++++++++++++++++++-----------
 src/laws.h          |  1 +
 src/laws.test.c     | 30 ++++++++++++++++++++++++++++++
 src/study.test.c    | 20 ++++++++++----------
 7 files changed, 82 insertions(+), 30 deletions(-)

diff --git a/clibs b/clibs
index f9842e07a..9b6e34959 160000
--- a/clibs
+++ b/clibs
@@ -1 +1 @@
-Subproject commit f9842e07a442c5453c270badf25ab72633b4edf5
+Subproject commit 9b6e34959f77d7ca3a4ce3826cb487487f557441
diff --git a/src/automate.c b/src/automate.c
index 2f188f352..ec4ea83d8 100644
--- a/src/automate.c
+++ b/src/automate.c
@@ -1,6 +1,7 @@
 #include <platform.h>
 
 #include "kernel/faction.h"
+#include "kernel/messages.h"
 #include "kernel/order.h"
 #include "kernel/region.h"
 #include "kernel/unit.h"
@@ -9,6 +10,7 @@
 
 #include "automate.h"
 #include "keyword.h"
+#include "laws.h"
 #include "study.h"
 
 #include <stdlib.h>
@@ -34,15 +36,21 @@ int autostudy_init(scholar scholars[], int max_scholars, region *r)
     for (u = r->units; u; u = u->next) {
         keyword_t kwd = getkeyword(u->thisorder);
         if (kwd == K_AUTOSTUDY) {
-            scholar * st = scholars + nscholars;
-            if (++nscholars == max_scholars) {
-                log_fatal("you must increase MAXSCHOLARS");
+            if (long_order_allowed(u) && unit_can_study(u)) {
+                scholar * st = scholars + nscholars;
+                if (++nscholars == max_scholars) {
+                    log_fatal("you must increase MAXSCHOLARS");
+                }
+                st->u = u;
+                init_order(u->thisorder, u->faction->locale);
+                st->sk = getskill(u->faction->locale);
+                st->level = effskill_study(u, st->sk);
+                st->learn = 0;
+            }
+            else {
+                ADDMSG(&u->faction->msgs, msg_feedback(u, u->thisorder, "error_race_nolearn", "race",
+                    u_race(u)));
             }
-            st->u = u;
-            init_order(u->thisorder, u->faction->locale);
-            st->sk = getskill(u->faction->locale);
-            st->level = effskill_study(u, st->sk);
-            st->learn = 0;
         }
     }
     if (nscholars > 0) {
@@ -54,11 +62,13 @@ int autostudy_init(scholar scholars[], int max_scholars, region *r)
 static void teaching(scholar *s, int n) {
     assert(n <= s->u->number);
     s->learn += n;
+    fset(s->u, UFL_LONGACTION);
 }
 
 static void learning(scholar *s, int n) {
     assert(n <= s->u->number);
     s->learn += n;
+    fset(s->u, UFL_LONGACTION);
 }
 
 void autostudy_run(scholar scholars[], int nscholars)
diff --git a/src/automate.test.c b/src/automate.test.c
index 999cb25d4..a77376668 100644
--- a/src/automate.test.c
+++ b/src/automate.test.c
@@ -73,6 +73,7 @@ static void test_autostudy_run(CuTest *tc) {
     CuAssertIntEquals(tc, 0, scholars[0].learn);
     CuAssertIntEquals(tc, 20, scholars[1].learn);
     CuAssertIntEquals(tc, 15, scholars[2].learn);
+    test_teardown();
 }
 
 static void test_autostudy_run_noteachers(CuTest *tc) {
@@ -100,6 +101,7 @@ static void test_autostudy_run_noteachers(CuTest *tc) {
     CuAssertIntEquals(tc, 2, scholars[0].learn);
     CuAssertIntEquals(tc, 10, scholars[1].learn);
     CuAssertIntEquals(tc, 15, scholars[2].learn);
+    test_teardown();
 }
 
 CuSuite *get_automate_suite(void)
diff --git a/src/laws.c b/src/laws.c
index 3902e9a8e..3280d91bf 100644
--- a/src/laws.c
+++ b/src/laws.c
@@ -3647,6 +3647,24 @@ void add_proc_unit(int priority, void(*process) (unit *), const char *name)
     }
 }
 
+bool long_order_allowed(const unit *u)
+{
+    const region *r = u->region;
+    if (fval(u, UFL_LONGACTION)) {
+        /* this message was already given in laws.update_long_order
+        cmistake(u, ord, 52, MSG_PRODUCE);
+        */
+        return false;
+    }
+    else if (fval(r->terrain, SEA_REGION)
+        && u_race(u) != get_race(RC_AQUARIAN)
+        && !(u_race(u)->flags & RCF_SWIM)) {
+        /* error message disabled by popular demand */
+        return false;
+    }
+    return true;
+}
+
 /* per priority, execute processors in order from PR_GLOBAL down to PR_ORDER */
 void process(void)
 {
@@ -3716,16 +3734,7 @@ void process(void)
                                         cmistake(u, ord, 224, MSG_MAGIC);
                                         ord = NULL;
                                     }
-                                    else if (fval(u, UFL_LONGACTION)) {
-                                        /* this message was already given in laws.update_long_order
-                                           cmistake(u, ord, 52, MSG_PRODUCE);
-                                           */
-                                        ord = NULL;
-                                    }
-                                    else if (fval(r->terrain, SEA_REGION)
-                                        && u_race(u) != get_race(RC_AQUARIAN)
-                                        && !(u_race(u)->flags & RCF_SWIM)) {
-                                        /* error message disabled by popular demand */
+                                    else if (!long_order_allowed(u)) {
                                         ord = NULL;
                                     }
                                 }
@@ -4167,7 +4176,7 @@ void update_subscriptions(void)
 /** determine if unit can be seen by faction
  * @param f -- the observiong faction
  * @param u -- the unit that is observed
- * @param r -- the region that u is obesrved in (see below)
+ * @param r -- the region that u is obesrved from (see below)
  * @param m -- terrain modifier to stealth
  * 
  * r kann != u->region sein, wenn es um Durchreisen geht,
diff --git a/src/laws.h b/src/laws.h
index ae2c712ca..0b5db17c7 100755
--- a/src/laws.h
+++ b/src/laws.h
@@ -66,6 +66,7 @@ extern "C" {
     void update_long_order(struct unit *u);
     void sinkships(struct region * r);
     void do_enter(struct region *r, bool is_final_attempt);
+    bool long_order_allowed(const struct unit *u);
 
     int password_cmd(struct unit *u, struct order *ord);
     int banner_cmd(struct unit *u, struct order *ord);
diff --git a/src/laws.test.c b/src/laws.test.c
index 8d13b7c81..c7c7a6682 100644
--- a/src/laws.test.c
+++ b/src/laws.test.c
@@ -1759,6 +1759,34 @@ static void test_nmr_timeout(CuTest *tc) {
     test_teardown();
 }
 
+static void test_long_orders(CuTest *tc) {
+    unit *u;
+
+    test_setup();
+    u = test_create_unit(test_create_faction(NULL), test_create_plain(0, 0));
+    CuAssertTrue(tc, long_order_allowed(u));
+    u->flags |= UFL_LONGACTION;
+    CuAssertTrue(tc, !long_order_allowed(u));
+    test_teardown();
+}
+
+static void test_long_order_on_ocean(CuTest *tc) {
+    unit *u;
+    race * rc;
+
+    test_setup();
+    rc = test_create_race("pikachu");
+    u = test_create_unit(test_create_faction(rc), test_create_ocean(0, 0));
+    CuAssertTrue(tc, !long_order_allowed(u));
+    rc->flags |= RCF_SWIM;
+    CuAssertTrue(tc, long_order_allowed(u));
+
+    rc = test_create_race("aquarian");
+    u = test_create_unit(test_create_faction(rc), u->region);
+    CuAssertTrue(tc, long_order_allowed(u));
+    test_teardown();
+}
+
 CuSuite *get_laws_suite(void)
 {
     CuSuite *suite = CuSuiteNew();
@@ -1831,6 +1859,8 @@ CuSuite *get_laws_suite(void)
     SUITE_ADD_TEST(suite, test_cansee_ring);
     SUITE_ADD_TEST(suite, test_cansee_sphere);
     SUITE_ADD_TEST(suite, test_nmr_timeout);
+    SUITE_ADD_TEST(suite, test_long_orders);
+    SUITE_ADD_TEST(suite, test_long_order_on_ocean);
 
     return suite;
 }
diff --git a/src/study.test.c b/src/study.test.c
index c1d0b0841..a63be7cf1 100644
--- a/src/study.test.c
+++ b/src/study.test.c
@@ -89,15 +89,15 @@ static void setup_teacher(study_fixture *fix, skill_t sk) {
     setup_locale(lang);
     fix->u = test_create_unit(f, r);
     assert(fix->u);
-    fix->u->thisorder = create_order(K_STUDY, f->locale, "%s", skillnames[sk]);
+    fix->u->thisorder = create_order(K_STUDY, f->locale, skillnames[sk]);
 
     fix->teachers[0] = test_create_unit(f, r);
     assert(fix->teachers[0]);
-    fix->teachers[0]->thisorder = create_order(K_TEACH, f->locale, "%s", itoa36(fix->u->no));
+    fix->teachers[0]->thisorder = create_order(K_TEACH, f->locale, itoa36(fix->u->no));
 
     fix->teachers[1] = test_create_unit(f, r);
     assert(fix->teachers[1]);
-    fix->teachers[1]->thisorder = create_order(K_TEACH, f->locale, "%s", itoa36(fix->u->no));
+    fix->teachers[1]->thisorder = create_order(K_TEACH, f->locale, itoa36(fix->u->no));
     test_clear_messages(f);
 }
 
@@ -110,7 +110,7 @@ static void test_study_no_teacher(CuTest *tc) {
     CuAssertPtrNotNull(tc, sv = unit_skill(fix.u, SK_CROSSBOW));
     CuAssertIntEquals(tc, 1, sv->level);
     CuAssertIntEquals(tc, 2, sv->weeks);
-    CuAssertPtrEquals(tc, 0, test_get_last_message(fix.u->faction->msgs));
+    CuAssertPtrEquals(tc, NULL, test_get_last_message(fix.u->faction->msgs));
     test_teardown();
 }
 
@@ -121,7 +121,7 @@ static void test_study_with_teacher(CuTest *tc) {
     setup_teacher(&fix, SK_CROSSBOW);
     set_level(fix.teachers[0], SK_CROSSBOW, TEACHDIFFERENCE);
     teach_cmd(fix.teachers[0], fix.teachers[0]->thisorder);
-    CuAssertPtrEquals(tc, 0, test_get_last_message(fix.u->faction->msgs));
+    CuAssertPtrEquals(tc, NULL, test_get_last_message(fix.u->faction->msgs));
     study_cmd(fix.u, fix.u->thisorder);
     CuAssertPtrNotNull(tc, sv = unit_skill(fix.u, SK_CROSSBOW));
     CuAssertIntEquals(tc, 1, sv->level);
@@ -288,7 +288,7 @@ static void test_academy_bonus(CuTest *tc) {
     u1 = test_create_unit(u->faction, u->region);
     u3 = test_create_unit(u->faction, u->region);
     u0->thisorder = create_order(K_TEACH, loc, "%s %s", itoa36(u3->no), itoa36(u1->no));
-    u->thisorder = create_order(K_TEACH, loc, "%s", itoa36(u1->no));
+    u->thisorder = create_order(K_TEACH, loc, itoa36(u1->no));
     u1->thisorder = create_order(K_STUDY, loc, skillnames[SK_CROSSBOW]);
     u3->thisorder = create_order(K_STUDY, loc, skillnames[SK_CROSSBOW]);
     
@@ -405,7 +405,7 @@ static void test_study_magic(CuTest *tc) {
     f = test_create_faction(NULL);
     lang = f->locale;
     u = test_create_unit(f, test_create_region(0, 0, NULL));
-    u->thisorder = create_order(K_STUDY, lang, "%s", skillnames[SK_MAGIC]);
+    u->thisorder = create_order(K_STUDY, lang, skillnames[SK_MAGIC]);
     itype = test_create_silver();
 
     CuAssertIntEquals(tc, -1, study_cmd(u, u->thisorder));
@@ -423,7 +423,7 @@ static void test_study_magic(CuTest *tc) {
     CuAssertIntEquals(tc, M_GWYRRD, f->magiegebiet);
     CuAssertIntEquals(tc, 0, i_get(u->items, itype));
     CuAssertPtrNotNull(tc, get_mage_depr(u));
-    CuAssertPtrEquals(tc, 0, test_find_messagetype(f->msgs, "error65"));
+    CuAssertPtrEquals(tc, NULL, test_find_messagetype(f->msgs, "error65"));
     CuAssertIntEquals(tc, M_GWYRRD, get_mage_depr(u)->magietyp);
 
     test_teardown();
@@ -491,12 +491,12 @@ static void test_teach_magic(CuTest *tc) {
     f = test_create_faction(NULL);
     f->magiegebiet = M_GWYRRD;
     u = test_create_unit(f, test_create_region(0, 0, NULL));
-    u->thisorder = create_order(K_STUDY, f->locale, "%s", skillnames[SK_MAGIC]);
+    u->thisorder = create_order(K_STUDY, f->locale, skillnames[SK_MAGIC]);
     i_change(&u->items, itype, study_cost(u, SK_MAGIC));
     ut = test_create_unit(f, u->region);
     set_level(ut, SK_MAGIC, TEACHDIFFERENCE);
     create_mage(ut, M_GWYRRD);
-    ut->thisorder = create_order(K_TEACH, f->locale, "%s", itoa36(u->no));
+    ut->thisorder = create_order(K_TEACH, f->locale, itoa36(u->no));
     learn_inject();
     teach_cmd(ut, ut->thisorder);
     study_cmd(u, u->thisorder);

From 820264aa8214af81ce5bdb8c7e252af5079fbe55 Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Sun, 15 Jul 2018 13:39:23 +0200
Subject: [PATCH 31/62] BUG 2461: Liste der Angreifer zeigt nicht alle
 Parteien.

---
 src/battle.c      |   2 +-
 src/battle.h      |   1 +
 src/battle.test.c | 104 ++++++++++++++++++++++++++++++++++++++++++++++
 src/reports.c     |  10 +++--
 4 files changed, 112 insertions(+), 5 deletions(-)

diff --git a/src/battle.c b/src/battle.c
index be950af7f..38590b8a6 100644
--- a/src/battle.c
+++ b/src/battle.c
@@ -3323,7 +3323,7 @@ fighter * get_fighter(battle * b, const struct unit * u)
     return 0;
 }
 
-static int join_battle(battle * b, unit * u, bool attack, fighter ** cp)
+int join_battle(battle * b, unit * u, bool attack, fighter ** cp)
 {
     side *s;
     fighter *fc = NULL;
diff --git a/src/battle.h b/src/battle.h
index c9429a6b3..48fa7c63a 100644
--- a/src/battle.h
+++ b/src/battle.h
@@ -261,6 +261,7 @@ extern "C" {
     void free_battle(struct battle * b);
     struct fighter *make_fighter(struct battle *b, struct unit *u,
     struct side * s, bool attack);
+    int join_battle(struct battle * b, struct unit * u, bool attack, struct fighter ** cp);
     struct side *make_side(struct battle * b, const struct faction * f,
         const struct group * g, unsigned int flags,
         const struct faction * stealthfaction);
diff --git a/src/battle.test.c b/src/battle.test.c
index 56ca24ac4..7c7012e77 100644
--- a/src/battle.test.c
+++ b/src/battle.test.c
@@ -1,6 +1,8 @@
 #include <platform.h>
 
 #include "battle.h"
+
+#include "reports.h"
 #include "skill.h"
 
 #include <kernel/config.h>
@@ -16,6 +18,8 @@
 #include <spells/buildingcurse.h>
 
 #include <util/functions.h>
+#include <util/language.h>
+#include <util/message.h>
 #include <util/rand.h>
 #include <util/rng.h>
 #include <util/strings.h>
@@ -552,6 +556,103 @@ static void test_battle_skilldiff(CuTest *tc)
     test_teardown();
 }
 
+static void test_battle_report_one(CuTest *tc)
+{
+    battle * b = NULL;
+    region *r;
+    unit *u1, *u2;
+    message *m;
+    const char *expect;
+    fighter *fig;
+
+    test_setup();
+    mt_create_va(mt_new("start_battle", NULL), "factions:string", MT_NEW_END);
+    r = test_create_plain(0, 0);
+    u1 = test_create_unit(test_create_faction(NULL), r);
+    u2 = test_create_unit(test_create_faction(NULL), r);
+    b = make_battle(r);
+    join_battle(b, u1, true, &fig);
+    join_battle(b, u2, false, &fig);
+    faction_setname(u1->faction, "Monster");
+    expect = factionname(u1->faction);
+
+    report_battle_start(b);
+    CuAssertPtrNotNull(tc, m = test_find_messagetype(u1->faction->battles->msgs, "start_battle"));
+    CuAssertStrEquals(tc, expect, (const char *)m->parameters[0].v);
+
+    free_battle(b);
+    test_teardown();
+}
+
+static void test_battle_report_two(CuTest *tc)
+{
+    battle * b = NULL;
+    region *r;
+    unit *u1, *u2;
+    message *m;
+    char expect[64];
+    fighter *fig;
+    struct locale *lang;
+
+    test_setup();
+    lang = test_create_locale();
+    locale_setstring(lang, "and", "and");
+    mt_create_va(mt_new("start_battle", NULL), "factions:string", MT_NEW_END);
+    r = test_create_plain(0, 0);
+    u1 = test_create_unit(test_create_faction(NULL), r);
+    u1->faction->locale = lang;
+    u2 = test_create_unit(test_create_faction(NULL), r);
+    u2->faction->locale = lang;
+
+    str_slprintf(expect, sizeof(expect), "%s and %s", factionname(u1->faction), factionname(u2->faction));
+    b = make_battle(r);
+    join_battle(b, u1, true, &fig);
+    join_battle(b, u2, true, &fig);
+    report_battle_start(b);
+
+    CuAssertPtrNotNull(tc, m = test_find_messagetype(u1->faction->battles->msgs, "start_battle"));
+    CuAssertStrEquals(tc, expect, (const char *)m->parameters[0].v);
+
+    free_battle(b);
+    test_teardown();
+}
+
+static void test_battle_report_three(CuTest *tc)
+{
+    battle * b = NULL;
+    region *r;
+    unit *u1, *u2, *u3;
+    message *m;
+    char expect[64];
+    fighter *fig;
+    struct locale *lang;
+
+    test_setup();
+    lang = test_create_locale();
+    locale_setstring(lang, "and", "and");
+    mt_create_va(mt_new("start_battle", NULL), "factions:string", MT_NEW_END);
+    r = test_create_plain(0, 0);
+    u1 = test_create_unit(test_create_faction(NULL), r);
+    u1->faction->locale = lang;
+    u2 = test_create_unit(test_create_faction(NULL), r);
+    u2->faction->locale = lang;
+    u3 = test_create_unit(test_create_faction(NULL), r);
+    u3->faction->locale = lang;
+
+    str_slprintf(expect, sizeof(expect), "%s, %s and %s", factionname(u1->faction), factionname(u2->faction), factionname(u3->faction));
+    b = make_battle(r);
+    join_battle(b, u1, true, &fig);
+    join_battle(b, u2, true, &fig);
+    join_battle(b, u3, true, &fig);
+    report_battle_start(b);
+
+    CuAssertPtrNotNull(tc, m = test_find_messagetype(u1->faction->battles->msgs, "start_battle"));
+    CuAssertStrEquals(tc, expect, (const char *)m->parameters[0].v);
+
+    free_battle(b);
+    test_teardown();
+}
+
 static void test_battle_skilldiff_building(CuTest *tc)
 {
     troop ta, td;
@@ -690,6 +791,9 @@ CuSuite *get_battle_suite(void)
     SUITE_ADD_TEST(suite, test_select_armor);
     SUITE_ADD_TEST(suite, test_battle_skilldiff);
     SUITE_ADD_TEST(suite, test_battle_skilldiff_building);
+    SUITE_ADD_TEST(suite, test_battle_report_one);
+    SUITE_ADD_TEST(suite, test_battle_report_two);
+    SUITE_ADD_TEST(suite, test_battle_report_three);
     SUITE_ADD_TEST(suite, test_defenders_get_building_bonus);
     SUITE_ADD_TEST(suite, test_attackers_get_no_building_bonus);
     SUITE_ADD_TEST(suite, test_building_bonus_respects_size);
diff --git a/src/reports.c b/src/reports.c
index d240ee86d..5355a1db9 100644
--- a/src/reports.c
+++ b/src/reports.c
@@ -2193,10 +2193,12 @@ void report_battle_start(battle * b)
                 }
             }
         }
-        if (first && lastf) {
-            sbs_strcat(&sbs, " ");
-            sbs_strcat(&sbs, LOC(f->locale, "and"));
-            sbs_strcat(&sbs, " ");
+        if (lastf) {
+            if (first) {
+                sbs_strcat(&sbs, " ");
+                sbs_strcat(&sbs, LOC(f->locale, "and"));
+                sbs_strcat(&sbs, " ");
+            }
             sbs_strcat(&sbs, lastf);
         }
 

From 8a503445f61f5c568b7c4bfa0cb1a59c5892b849 Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno@eressea.de>
Date: Mon, 16 Jul 2018 10:53:12 +0200
Subject: [PATCH 32/62] missing include, do not use fset

---
 src/.DS_Store      | Bin 0 -> 8196 bytes
 src/automate.c     |   4 ++--
 src/util/.DS_Store | Bin 0 -> 6148 bytes
 3 files changed, 2 insertions(+), 2 deletions(-)
 create mode 100644 src/.DS_Store
 create mode 100644 src/util/.DS_Store

diff --git a/src/.DS_Store b/src/.DS_Store
new file mode 100644
index 0000000000000000000000000000000000000000..f331cdebb3c73099a3bcd9d817441024e2973339
GIT binary patch
literal 8196
zcmeHMTWl0n82-QQg&8{1=~`i1ob1-67O{kuAOumny>V+v8`{z>8p`g@2&2=TvNO9y
zBxW_yXbe%`Pz?Iui>a3v1$|UsjQU_?UudEx>We<83BHgR<9}vmNlWR2i7`axB<Fl{
z{{KIh`Q|%w_RJCy`igotQH+Qr&H~j2E^d&7U*u&e5?E>&!R^U%O8r^O&Jsl`%c4U=
z$Uw+I$Uw+I$Uw-z&A<Tf*&<0R?EAhqEJFrD25w0P#QPz^Szs!_vX9=YgA4Bnz*x&>
zFY&c1E*KL5rUESc$R1Acn=<-M5gak#H)neyh)V@n_R()n2o4_zMn-T#fj2t$pYXy7
zDIdc!WFTZ<IRoPCUQ0!C$s}v>`8_*pnU34lwSY)XZC(8;NnYKc+@|dx8a|+mOsmn7
zY0Qnxx~83X4jBc9aawJ3j@xtla&~^SU<m(Y&bEp(d1qQ}%4Muv(aN|+uVsyyrwq0=
zI=O;vS);aNif*RTu5r>8zGbfTVaG1G{a$C&s#<i6g9i6ymW%=J(Mg*;q-dx@o?o*r
zzG36$tven_4OSY}rl``ateq^Gj+wQLi8)huk56TsoRQbf{ISEPZsc9_QPU{MP3V-z
z^O+f=5{<RAuD?^$u8*$vk}hV=6GhW~LQP6iC8{<xCfYRR5uK-Uz}7`?y859cd!F_-
zO?y~)`U?h^IydkpwMpxg)r>Um@T`5y7NuKO9eMn5Gp~CVZJV-PQgwO4%47}8H|<n*
zN$QY1S@Ie8jm=7rB&T!s%naX*;|<%}r)fvTu)`V09ho)qJf+Wh?ABOIRBO}rO45|@
z_UbxMDVaJo>j@*7+UN@;S=EV0h@4Dy1qO*U+DZd7Oh>6ePtqAWOY`&rU7(Bf8GS_+
zTA-ikH~O8f&>wUa5lD!l6-l(?Zgijv526Pt^kP2_VG?O%kcAEdkHLY966SCk&*C{e
zj~DO~&f-;^!|Qkp@8UhYkB{&PzQ#AWgbKdJclaJZ;}=}PpOL!A>PYZAHWF+RG+Mn4
zA0y3-i?u!*e5*QlW39(`@7cRg8M=A3etC`1((TQ6tX-FA-?U}h&WHN0$>6%DzgUBw
z=zLXqYOAWrd)P~4T=7?e5}#mE8Yfp;5b7$ic%;humeyGOuJ{ItwWbEuj`%$iuLd>Z
z^>uG&Tv2OS__9~+>Q>@v9cx_js`qVI6t$KYf$zP47klej?!LE2V=u8!45M_!8)WI<
zlI|CDiGHA8S+rN_FVv#}F|@H<H)99OwHE^z#BPjW6bCVe2^_(6Alw<)IDrC-_cWeD
z8BgOGyofU_-IwtSUSsK=#~XMPZ(|<su!t|<B8&KAe2Onw!k4iSNV6*0;iZzrkBcQ+
z8Y|>2JAaIA)%d01VJ&3fA21;Heois*-W*){`@a_y79j&61OHD3P&+U-(8u?-(yk)T
z+6m4_Ig2FX%07BcxLEQbfa}-(Fy!k5hl)0-0LwnIh0}lkAz=AW*Kq&$9z|H(LJa&3
DfC<08

literal 0
HcmV?d00001

diff --git a/src/automate.c b/src/automate.c
index ec4ea83d8..a74cf770b 100644
--- a/src/automate.c
+++ b/src/automate.c
@@ -62,13 +62,13 @@ int autostudy_init(scholar scholars[], int max_scholars, region *r)
 static void teaching(scholar *s, int n) {
     assert(n <= s->u->number);
     s->learn += n;
-    fset(s->u, UFL_LONGACTION);
+    s->u->flags |= UFL_LONGACTION;
 }
 
 static void learning(scholar *s, int n) {
     assert(n <= s->u->number);
     s->learn += n;
-    fset(s->u, UFL_LONGACTION);
+    s->u->flags |= UFL_LONGACTION;
 }
 
 void autostudy_run(scholar scholars[], int nscholars)
diff --git a/src/util/.DS_Store b/src/util/.DS_Store
new file mode 100644
index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6
GIT binary patch
literal 6148
zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3
zem<@ulZcFPQ@L2!n>{z**<q8>++&mCkOWA81W14cNZ<zv;LbK1Poaz?KmsK2CSc!(
z0ynLxE!0092;Krf2c+FF_Fe*7ECH>lEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ
zLs35+`xjp>T0<F0fCPF1$Cyrb|F7^5{eNG?83~ZUUlGt@xh*qZDeu<Z%US-OSsOPv
j)R!Z4KLME7ReXlK;d!wEw5GODWMKRea10D2@KpjYNUI8I

literal 0
HcmV?d00001


From fc80e4b83eb4015de12546240667836ac4f7fc43 Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Tue, 17 Jul 2018 20:53:34 +0200
Subject: [PATCH 33/62] =?UTF-8?q?BUG=202462:=20Leuchtturm=20meldet=20keine?=
 =?UTF-8?q?=20Landregionen,=20besch=C3=BCtzt=20Schiffe=20erst=20ab=20Gr?=
 =?UTF-8?q?=C3=B6=C3=9Fe=2010.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 scripts/tests/report.lua |   6 +--
 src/kernel/build.c       |   4 +-
 src/kernel/building.c    |  12 +++--
 src/lighthouse.c         | 114 ++++++++++++++++++---------------------
 src/lighthouse.h         |   5 +-
 src/lighthouse.test.c    |  43 +++++++++++++--
 src/move.c               |   4 +-
 src/reports.c            |  19 ++++---
 src/reports.test.c       |   4 +-
 9 files changed, 125 insertions(+), 86 deletions(-)

diff --git a/scripts/tests/report.lua b/scripts/tests/report.lua
index 12d1538c3..b65bcc279 100644
--- a/scripts/tests/report.lua
+++ b/scripts/tests/report.lua
@@ -93,11 +93,11 @@ end
 function test_lighthouse()
     eressea.free_game()
     local r = region.create(0, 0, "mountain")
-    local f = faction.create("human", "noreply@eressea.de", "de")
+    local f = faction.create("human", "human@example.com")
     region.create(1, 0, "mountain")
     region.create(2, 0, "ocean")
     region.create(0, 1, "firewall")
-    region.create(3, 0, "mountain")
+    region.create(3, 0, "ocean")
     region.create(4, 0, "plain")
     local u = unit.create(f, r, 1)
     local b = building.create(r, "lighthouse")
@@ -110,7 +110,7 @@ function test_lighthouse()
 
     init_reports()
     write_report(f)
-    assert_true(find_in_report(f, " %(1,0%) %(vom Turm erblickt%)"))
+    assert_false(find_in_report(f, " %(1,0%) %(vom Turm erblickt%)"))
     assert_true(find_in_report(f, " %(2,0%) %(vom Turm erblickt%)"))
     assert_true(find_in_report(f, " %(3,0%) %(vom Turm erblickt%)"))
 
diff --git a/src/kernel/build.c b/src/kernel/build.c
index 310e4bd43..11324f752 100644
--- a/src/kernel/build.c
+++ b/src/kernel/build.c
@@ -895,7 +895,9 @@ build_building(unit * u, const building_type * btype, int id, int want, order *
     }
     fset(b, BLD_EXPANDED);
 
-    update_lighthouse(b);
+    if (is_lighthouse(btype)) {
+        update_lighthouse(b);
+    }
 
     return built;
 }
diff --git a/src/kernel/building.c b/src/kernel/building.c
index f3f01905f..75cad30f7 100644
--- a/src/kernel/building.c
+++ b/src/kernel/building.c
@@ -377,7 +377,9 @@ building *new_building(const struct building_type * btype, region * r,
         bptr = &(*bptr)->next;
     *bptr = b;
 
-    update_lighthouse(b);
+    if (is_lighthouse(b->type)) {
+        update_lighthouse(b);
+    }
     bname = LOC(lang, btype->_name);
     if (!bname) {
         bname = LOC(lang, parameters[P_GEBAEUDE]);
@@ -399,6 +401,7 @@ static building *deleted_buildings;
 void remove_building(building ** blist, building * b)
 {
     unit *u;
+    region *r = b->region;
     static const struct building_type *bt_caravan, *bt_dam, *bt_tunnel;
     static int btypes;
 
@@ -410,18 +413,19 @@ void remove_building(building ** blist, building * b)
         bt_tunnel = bt_find("tunnel");
     }
     handle_event(b->attribs, "destroy", b);
-    for (u = b->region->units; u; u = u->next) {
+    for (u = r->units; u; u = u->next) {
         if (u->building == b) leave(u, true);
     }
 
+    if (is_lighthouse(b->type)) {
+        remove_lighthouse(b);
+    }
     b->size = 0;
-    update_lighthouse(b);
     bunhash(b);
 
     /* Falls Karawanserei, Damm oder Tunnel einst�rzen, wird die schon
      * gebaute Strasse zur Haelfte vernichtet */
     if (b->type == bt_caravan || b->type == bt_dam || b->type == bt_tunnel) {
-        region *r = b->region;
         int d;
         for (d = 0; d != MAXDIRECTIONS; ++d) {
             direction_t dir = (direction_t)d;
diff --git a/src/lighthouse.c b/src/lighthouse.c
index fbf950fb1..2ff1c6191 100644
--- a/src/lighthouse.c
+++ b/src/lighthouse.c
@@ -19,6 +19,11 @@ attrib_type at_lighthouse = {
     /* Rest ist NULL; tempor�res, nicht alterndes Attribut */
 };
 
+bool is_lighthouse(const building_type *btype)
+{
+    return is_building_type(btype, "lighthouse");
+}
+
 /* update_lighthouse: call this function whenever the size of a lighthouse changes
 * it adds temporary markers to the surrounding regions.
 * The existence of markers says nothing about the quality of the observer in
@@ -26,46 +31,57 @@ attrib_type at_lighthouse = {
 */
 void update_lighthouse(building * lh)
 {
-    if (is_building_type(lh->type, "lighthouse")) {
-        region *r = lh->region;
+    region *r = lh->region;
+    assert(is_lighthouse(lh->type));
 
-        r->flags |= RF_LIGHTHOUSE;
-        if (lh->size > 0) {
-            int d = (int)log10(lh->size) + 1;
-            int x;
-            for (x = -d; x <= d; ++x) {
-                int y;
-                for (y = -d; y <= d; ++y) {
-                    attrib *a;
-                    region *r2;
-                    int px = r->x + x, py = r->y + y;
+    r->flags |= RF_LIGHTHOUSE;
+    if (lh->size >= 10) {
+        int d = lighthouse_range(lh, NULL, NULL);
+        int x;
+        for (x = -d; x <= d; ++x) {
+            int y;
+            for (y = -d; y <= d; ++y) {
+                attrib *a;
+                region *r2;
+                int px = r->x + x, py = r->y + y;
 
-                    pnormalize(&px, &py, rplane(r));
-                    r2 = findregion(px, py);
-                    if (!r2 || !fval(r2->terrain, SEA_REGION))
-                        continue;
-                    if (distance(r, r2) > d)
-                        continue;
-                    a = a_find(r2->attribs, &at_lighthouse);
-                    while (a && a->type == &at_lighthouse) {
-                        building *b = (building *)a->data.v;
-                        if (b == lh)
-                            break;
-                        a = a->next;
-                    }
-                    if (!a) {
-                        a = a_add(&r2->attribs, a_new(&at_lighthouse));
-                        a->data.v = (void *)lh;
-                    }
+                pnormalize(&px, &py, rplane(r));
+                r2 = findregion(px, py);
+                if (!r2 || !fval(r2->terrain, SEA_REGION))
+                    continue;
+                if (distance(r, r2) > d)
+                    continue;
+                a = a_find(r2->attribs, &at_lighthouse);
+                while (a && a->type == &at_lighthouse) {
+                    building *b = (building *)a->data.v;
+                    if (b == lh)
+                        break;
+                    a = a->next;
+                }
+                if (!a) {
+                    a = a_add(&r2->attribs, a_new(&at_lighthouse));
+                    a->data.v = (void *)lh;
                 }
             }
         }
     }
 }
 
+void remove_lighthouse(const building *lh) {
+    building *b;
+    region * r = lh->region;
+
+    r->flags &= ~RF_LIGHTHOUSE;
+    for (b = r->buildings; b; b = b->next) {
+        if (b != lh && is_lighthouse(b->type)) {
+            update_lighthouse(b);
+        }
+    }
+}
+
 int lighthouse_range(const building * b, const faction * f, const unit *u)
 {
-    if (fval(b, BLD_MAINTAINED) && b->size >= 10) {
+    if (b->size >= 10 && (b->flags & BLD_MAINTAINED)) {
         int maxd = (int)log10(b->size) + 1;
 
         if (u && skill_enabled(SK_PERCEPTION)) {
@@ -80,11 +96,11 @@ int lighthouse_range(const building * b, const faction * f, const unit *u)
     return 0;
 }
 
-bool check_leuchtturm(region * r, faction * f)
+bool lighthouse_guarded(const region * r)
 {
     attrib *a;
 
-    if (!fval(r->terrain, SEA_REGION)) {
+    if (!r->attribs || !(r->terrain->flags & SEA_REGION)) {
         return false;
     }
     for (a = a_find(r->attribs, &at_lighthouse); a && a->type == &at_lighthouse;
@@ -92,37 +108,11 @@ bool check_leuchtturm(region * r, faction * f)
         building *b = (building *)a->data.v;
 
         assert(is_building_type(b->type, "lighthouse"));
-        if (fval(b, BLD_MAINTAINED) && b->size >= 10) {
+        if ((b->flags & BLD_MAINTAINED) && b->size >= 10) {
             int maxd = (int)log10(b->size) + 1;
-
-            if (skill_enabled(SK_PERCEPTION) && f) {
-                region *r2 = b->region;
-                unit *u;
-                int c = 0;
-                int d = 0;
-
-                for (u = r2->units; u; u = u->next) {
-                    if (u->building == b) {
-                        c += u->number;
-                        if (c > buildingcapacity(b))
-                            break;
-                        if (u->faction == f) {
-                            if (!d)
-                                d = distance(r, r2);
-                            if (maxd < d)
-                                break;
-                            if (effskill(u, SK_PERCEPTION, 0) >= d * 3)
-                                return true;
-                        }
-                    }
-                    else if (c)
-                        break;              /* first unit that's no longer in the house ends the search */
-                }
-            }
-            else {
-                /* E3A rule: no perception req'd */
-                return true;
-            }
+            int d = distance(r, b->region);
+            assert(maxd >= d);
+            return true;
         }
     }
 
diff --git a/src/lighthouse.h b/src/lighthouse.h
index 2eacfb7d8..cd5a02d04 100644
--- a/src/lighthouse.h
+++ b/src/lighthouse.h
@@ -29,13 +29,16 @@ extern "C" {
     struct faction;
     struct region;
     struct building;
+    struct building_type;
     struct unit;
     struct attrib;
 
     extern struct attrib_type at_lighthouse;
     /* leuchtturm */
-    bool check_leuchtturm(struct region *r, struct faction *f);
+    bool is_lighthouse(const struct building_type *btype);
+    bool lighthouse_guarded(const struct region *r);
     void update_lighthouse(struct building *b);
+    void remove_lighthouse(const struct building *lh);
     int lighthouse_range(const struct building *b, const struct faction *f,
         const struct unit *u);
 
diff --git a/src/lighthouse.test.c b/src/lighthouse.test.c
index e0c57ec1a..bcc7c0ab7 100644
--- a/src/lighthouse.test.c
+++ b/src/lighthouse.test.c
@@ -36,6 +36,7 @@ static void test_lighthouse_range(CuTest * tc)
     set_level(u1, SK_PERCEPTION, 3);
     set_level(u2, SK_PERCEPTION, 3);
     CuAssertIntEquals(tc, 0, lighthouse_range(b, NULL, NULL));
+
     b->flags |= BLD_MAINTAINED;
     CuAssertIntEquals(tc, 1, lighthouse_range(b, u1->faction, u1));
     set_level(u1, SK_PERCEPTION, 6);
@@ -54,7 +55,7 @@ static void test_lighthouse_range(CuTest * tc)
 
 static void test_lighthouse_update(CuTest * tc)
 {
-    region *r1, *r2, *r3;
+    region *r1, *r2, *r3, *r4;
     building *b;
     const struct terrain_type *t_ocean, *t_plain;
 
@@ -64,20 +65,23 @@ static void test_lighthouse_update(CuTest * tc)
     r1 = test_create_region(0, 0, t_plain);
     r2 = test_create_region(1, 0, t_ocean);
     r3 = test_create_region(2, 0, t_ocean);
+    r4 = test_create_region(0, 1, t_plain);
     b = test_create_building(r1, test_create_buildingtype("lighthouse"));
+    b->flags |= BLD_MAINTAINED;
     CuAssertIntEquals(tc, RF_LIGHTHOUSE, r1->flags&RF_LIGHTHOUSE);
     CuAssertPtrEquals(tc, NULL, r1->attribs);
     CuAssertPtrEquals(tc, NULL, r2->attribs);
     CuAssertPtrEquals(tc, NULL, r3->attribs);
+    CuAssertPtrEquals(tc, NULL, r4->attribs);
 
     r1->flags = 0;
-    b->size = 1;
+    b->size = 9; /* minimum size for any effect is 10 */
     update_lighthouse(b);
     CuAssertIntEquals(tc, RF_LIGHTHOUSE, r1->flags&RF_LIGHTHOUSE);
-    CuAssertPtrNotNull(tc, r2->attribs);
-    CuAssertPtrEquals(tc, (void *)&at_lighthouse, (void *)r2->attribs->type);
     CuAssertPtrEquals(tc, NULL, r1->attribs);
+    CuAssertPtrEquals(tc, NULL, r2->attribs);
     CuAssertPtrEquals(tc, NULL, r3->attribs);
+    CuAssertPtrEquals(tc, NULL, r4->attribs);
 
     a_removeall(&r2->attribs, NULL);
     r1->flags = 0;
@@ -88,6 +92,36 @@ static void test_lighthouse_update(CuTest * tc)
     CuAssertPtrEquals(tc, (void *)&at_lighthouse, (void *)r2->attribs->type);
     CuAssertPtrNotNull(tc, r3->attribs);
     CuAssertPtrEquals(tc, (void *)&at_lighthouse, (void *)r3->attribs->type);
+    CuAssertPtrEquals(tc, NULL, r4->attribs);
+    test_teardown();
+}
+
+static void test_lighthouse_guard(CuTest * tc) {
+    region *r1, *r2, *r3, *r4;
+    building *b;
+    const struct terrain_type *t_ocean, *t_plain;
+
+    test_setup();
+    t_ocean = test_create_terrain("ocean", SEA_REGION);
+    t_plain = test_create_terrain("plain", LAND_REGION);
+    r1 = test_create_region(0, 0, t_plain);
+    r2 = test_create_region(1, 0, t_ocean);
+    r3 = test_create_region(2, 0, t_ocean);
+    r4 = test_create_region(0, 1, t_plain);
+    b = test_create_building(r1, test_create_buildingtype("lighthouse"));
+    b->flags |= BLD_MAINTAINED;
+    b->size = 10;
+    CuAssertIntEquals(tc, 2, lighthouse_range(b, NULL, NULL));
+    update_lighthouse(b);
+    CuAssertIntEquals(tc, RF_LIGHTHOUSE, r1->flags&RF_LIGHTHOUSE);
+    CuAssertPtrEquals(tc, NULL, r1->attribs);
+    CuAssertPtrEquals(tc, (void *)&at_lighthouse, (void *)r2->attribs->type);
+    CuAssertPtrEquals(tc, (void *)&at_lighthouse, (void *)r3->attribs->type);
+    CuAssertPtrEquals(tc, NULL, r4->attribs);
+    CuAssertIntEquals(tc, false, lighthouse_guarded(r1));
+    CuAssertIntEquals(tc, true, lighthouse_guarded(r2));
+    CuAssertIntEquals(tc, true, lighthouse_guarded(r3));
+    CuAssertIntEquals(tc, false, lighthouse_guarded(r4));
     test_teardown();
 }
 
@@ -96,5 +130,6 @@ CuSuite *get_lighthouse_suite(void)
     CuSuite *suite = CuSuiteNew();
     SUITE_ADD_TEST(suite, test_lighthouse_range);
     SUITE_ADD_TEST(suite, test_lighthouse_update);
+    SUITE_ADD_TEST(suite, test_lighthouse_guard);
     return suite;
 }
diff --git a/src/move.c b/src/move.c
index 990d6fc14..205b52416 100644
--- a/src/move.c
+++ b/src/move.c
@@ -1779,7 +1779,7 @@ static void sail(unit * u, order * ord, region_list ** routep, bool drifting)
 
                 /* storms should be the first thing we do. */
                 stormchance = stormyness / shipspeed(sh, u);
-                if (check_leuchtturm(next_point, NULL)) {
+                if (lighthouse_guarded(next_point)) {
                     if (lighthouse_div > 0) {
                         stormchance /= lighthouse_div;
                     }
@@ -1867,7 +1867,7 @@ static void sail(unit * u, order * ord, region_list ** routep, bool drifting)
                 if (reason == SA_NO_INSECT) {
                     ADDMSG(&f->msgs, msg_message("detectforbidden", "unit region", u, sh->region));
                 }
-                else if (check_leuchtturm(current_point, NULL)) {
+                else if (lighthouse_guarded(current_point)) {
                     ADDMSG(&f->msgs, msg_message("sailnolandingstorm", "ship region", sh, next_point));
                 }
                 else {
diff --git a/src/reports.c b/src/reports.c
index 5355a1db9..5d22636e8 100644
--- a/src/reports.c
+++ b/src/reports.c
@@ -1353,6 +1353,13 @@ static void add_seen_nb(faction *f, region *r, seen_mode mode) {
     update_interval(f, last);
 }
 
+static void add_seen_lighthouse(region *r, faction *f)
+{
+    if (r->terrain->flags & SEA_REGION) {
+        add_seen_nb(f, r, seen_lighthouse);
+    }
+}
+
 /** mark all regions seen by the lighthouse.
  */
 static void prepare_lighthouse_ql(faction *f, selist *rlist) {
@@ -1361,9 +1368,7 @@ static void prepare_lighthouse_ql(faction *f, selist *rlist) {
 
     for (ql = rlist, qi = 0; ql; selist_advance(&ql, &qi, 1)) {
         region *rl = (region *)selist_get(ql, qi);
-        if (!fval(rl->terrain, FORBIDDEN_REGION)) {
-            add_seen_nb(f, rl, seen_lighthouse);
-        }
+        add_seen_lighthouse(rl, f);
     }
 }
 
@@ -1382,9 +1387,7 @@ static void prepare_lighthouse(faction *f, region *r, int range)
         assert(n > 0 && n <= 64);
         for (i = 0; i != n; ++i) {
             region *rl = result[i];
-            if (!fval(rl->terrain, FORBIDDEN_REGION)) {
-                add_seen_nb(f, rl, seen_lighthouse);
-            }
+            add_seen_lighthouse(rl, f);
         }
     }
 }
@@ -1592,7 +1595,7 @@ void prepare_report(report_context *ctx, faction *f)
                 if (rule_region_owners && f == region_get_owner(r)) {
                     for (b = rbuildings(r); b; b = b->next) {
                         if (b && b->type == bt_lighthouse) {
-                            /* region owners get maximm range */
+                            /* region owners get maximum range */
                             int lhr = lighthouse_range(b, NULL, NULL);
                             if (lhr > range) range = lhr;
                         }
@@ -1630,7 +1633,7 @@ void prepare_report(report_context *ctx, faction *f)
                         /* unit is one of ours, and inside the current lighthouse */
                         if (br == 0) {
                             /* lazy-calculate the range */
-                            br = lighthouse_range(u->building, f, u);
+                            br = lighthouse_range(b, f, u);
                         }
                         if (br > range) {
                             range = br;
diff --git a/src/reports.test.c b/src/reports.test.c
index 2b22782ae..6d7a7961f 100644
--- a/src/reports.test.c
+++ b/src/reports.test.c
@@ -530,7 +530,7 @@ void test_prepare_lighthouse_capacity(CuTest *tc) {
 static void test_prepare_lighthouse(CuTest *tc) {
     report_context ctx;
     faction *f;
-    region *r1, *r2, *r3;
+    region *r1, *r2, *r3, *r4;
     unit *u;
     building *b;
     building_type *btype;
@@ -543,6 +543,7 @@ static void test_prepare_lighthouse(CuTest *tc) {
     r1 = test_create_region(0, 0, t_plain);
     r2 = test_create_region(1, 0, t_ocean);
     r3 = test_create_region(2, 0, t_ocean);
+    r4 = test_create_region(0, 1, t_plain);
     btype = test_create_buildingtype("lighthouse");
     b = test_create_building(r1, btype);
     b->flags |= BLD_MAINTAINED;
@@ -557,6 +558,7 @@ static void test_prepare_lighthouse(CuTest *tc) {
     CuAssertIntEquals(tc, seen_unit, r1->seen.mode);
     CuAssertIntEquals(tc, seen_lighthouse, r2->seen.mode);
     CuAssertIntEquals(tc, seen_neighbour, r3->seen.mode);
+    CuAssertIntEquals(tc, seen_neighbour, r4->seen.mode);
     finish_reports(&ctx);
     test_teardown();
 }

From 7b7d3cb7597440d19722c56ac631e7392a414f26 Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Tue, 17 Jul 2018 21:34:22 +0200
Subject: [PATCH 34/62] test: lighhouse size reduction reduces effectiveness

---
 src/lighthouse.c      | 8 ++++----
 src/lighthouse.test.c | 3 +++
 2 files changed, 7 insertions(+), 4 deletions(-)

diff --git a/src/lighthouse.c b/src/lighthouse.c
index 2ff1c6191..ff1cc5b84 100644
--- a/src/lighthouse.c
+++ b/src/lighthouse.c
@@ -25,10 +25,10 @@ bool is_lighthouse(const building_type *btype)
 }
 
 /* update_lighthouse: call this function whenever the size of a lighthouse changes
-* it adds temporary markers to the surrounding regions.
-* The existence of markers says nothing about the quality of the observer in
-* the lighthouse, for this may change more frequently.
-*/
+ * it adds temporary markers to the surrounding regions.
+ * The existence of markers says nothing about the quality of the observer in
+ * the lighthouse, since this may change more frequently.
+ */
 void update_lighthouse(building * lh)
 {
     region *r = lh->region;
diff --git a/src/lighthouse.test.c b/src/lighthouse.test.c
index bcc7c0ab7..f85b5ce99 100644
--- a/src/lighthouse.test.c
+++ b/src/lighthouse.test.c
@@ -122,6 +122,9 @@ static void test_lighthouse_guard(CuTest * tc) {
     CuAssertIntEquals(tc, true, lighthouse_guarded(r2));
     CuAssertIntEquals(tc, true, lighthouse_guarded(r3));
     CuAssertIntEquals(tc, false, lighthouse_guarded(r4));
+    b->size = 1; /* size can go down in destroy_cmd */
+    CuAssertIntEquals(tc, false, lighthouse_guarded(r2));
+    CuAssertIntEquals(tc, false, lighthouse_guarded(r3));
     test_teardown();
 }
 

From 228216091619a9837fe3a0e6b25291a60419ca71 Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Wed, 18 Jul 2018 20:49:17 +0200
Subject: [PATCH 35/62] separate lighthouse_view_distance from
 lighthouse_range.

---
 src/lighthouse.c      | 18 ++++++++++++------
 src/lighthouse.h      |  4 ++--
 src/lighthouse.test.c | 31 +++++++++++++++++--------------
 src/reports.c         | 12 ++++++------
 src/reports.test.c    |  4 ++--
 5 files changed, 39 insertions(+), 30 deletions(-)

diff --git a/src/lighthouse.c b/src/lighthouse.c
index ff1cc5b84..4ef830cfc 100644
--- a/src/lighthouse.c
+++ b/src/lighthouse.c
@@ -36,7 +36,7 @@ void update_lighthouse(building * lh)
 
     r->flags |= RF_LIGHTHOUSE;
     if (lh->size >= 10) {
-        int d = lighthouse_range(lh, NULL, NULL);
+        int d = lighthouse_range(lh);
         int x;
         for (x = -d; x <= d; ++x) {
             int y;
@@ -79,18 +79,24 @@ void remove_lighthouse(const building *lh) {
     }
 }
 
-int lighthouse_range(const building * b, const faction * f, const unit *u)
+int lighthouse_range(const building * b)
 {
     if (b->size >= 10 && (b->flags & BLD_MAINTAINED)) {
-        int maxd = (int)log10(b->size) + 1;
+        return (int)log10(b->size) + 1;
+    }
+    return 0;
+}
 
-        if (u && skill_enabled(SK_PERCEPTION)) {
+int lighthouse_view_distance(const building * b, const unit *u)
+{
+    if (b->size >= 10 && (b->flags & BLD_MAINTAINED)) {
+        int maxd = lighthouse_range(b);
+
+        if (maxd > 0 && u && skill_enabled(SK_PERCEPTION)) {
             int sk = effskill(u, SK_PERCEPTION, 0) / 3;
             assert(u->building == b);
-            assert(u->faction == f);
             if (maxd > sk) maxd = sk;
         }
-        /* E3A rule: no perception req'd */
         return maxd;
     }
     return 0;
diff --git a/src/lighthouse.h b/src/lighthouse.h
index cd5a02d04..cd0e055fd 100644
--- a/src/lighthouse.h
+++ b/src/lighthouse.h
@@ -39,8 +39,8 @@ extern "C" {
     bool lighthouse_guarded(const struct region *r);
     void update_lighthouse(struct building *b);
     void remove_lighthouse(const struct building *lh);
-    int lighthouse_range(const struct building *b, const struct faction *f,
-        const struct unit *u);
+    int lighthouse_range(const struct building *b);
+    int lighthouse_view_distance(const struct building *b, const struct unit *u);
 
 
 #ifdef __cplusplus
diff --git a/src/lighthouse.test.c b/src/lighthouse.test.c
index f85b5ce99..d9accd49e 100644
--- a/src/lighthouse.test.c
+++ b/src/lighthouse.test.c
@@ -26,30 +26,33 @@ static void test_lighthouse_range(CuTest * tc)
     u1 = test_create_unit(test_create_faction(NULL), r);
     u2 = test_create_unit(test_create_faction(NULL), r);
     b = test_create_building(r, test_create_buildingtype("lighthouse"));
-    CuAssertIntEquals(tc, 0, lighthouse_range(b, NULL, NULL));
-    CuAssertIntEquals(tc, 0, lighthouse_range(b, u1->faction, NULL));
+    CuAssertIntEquals(tc, 0, lighthouse_range(b));
+    b->size = 9;
+    CuAssertIntEquals(tc, 0, lighthouse_range(b));
     b->size = 10;
-    CuAssertIntEquals(tc, 0, lighthouse_range(b, NULL, NULL));
+    CuAssertIntEquals(tc, 0, lighthouse_range(b));
+    b->flags |= BLD_MAINTAINED;
+    CuAssertIntEquals(tc, 2, lighthouse_range(b));
     u1->building = b;
     u2->building = b;
     u1->number = 10;
     set_level(u1, SK_PERCEPTION, 3);
     set_level(u2, SK_PERCEPTION, 3);
-    CuAssertIntEquals(tc, 0, lighthouse_range(b, NULL, NULL));
 
-    b->flags |= BLD_MAINTAINED;
-    CuAssertIntEquals(tc, 1, lighthouse_range(b, u1->faction, u1));
+    CuAssertIntEquals(tc, 1, lighthouse_view_distance(b, u1));
     set_level(u1, SK_PERCEPTION, 6);
-    CuAssertIntEquals(tc, 2, lighthouse_range(b, u1->faction, u1));
-    /* lighthouse_range does not check inside_building */
-    CuAssertIntEquals(tc, 1, lighthouse_range(b, u2->faction, u2));
+    CuAssertIntEquals(tc, 1, lighthouse_view_distance(b, u2));
+    CuAssertIntEquals(tc, 2, lighthouse_view_distance(b, u1));
     b->size = 100;
     update_lighthouse(b);
-    CuAssertIntEquals(tc, 3, lighthouse_range(b, NULL, NULL));
-    CuAssertIntEquals(tc, 2, lighthouse_range(b, u1->faction, u1));
+    CuAssertIntEquals(tc, 3, lighthouse_range(b));
+    CuAssertIntEquals(tc, 2, lighthouse_view_distance(b, u1));
     set_level(u1, SK_PERCEPTION, 9);
-    CuAssertIntEquals(tc, 3, lighthouse_range(b, u1->faction, u1));
-    CuAssertIntEquals(tc, 1, lighthouse_range(b, u2->faction, u2));
+    CuAssertIntEquals(tc, 3, lighthouse_view_distance(b, u1));
+    CuAssertIntEquals(tc, 1, lighthouse_view_distance(b, u2));
+    b->size = 99;
+    CuAssertIntEquals(tc, 2, lighthouse_view_distance(b, u1));
+
     test_teardown();
 }
 
@@ -111,7 +114,7 @@ static void test_lighthouse_guard(CuTest * tc) {
     b = test_create_building(r1, test_create_buildingtype("lighthouse"));
     b->flags |= BLD_MAINTAINED;
     b->size = 10;
-    CuAssertIntEquals(tc, 2, lighthouse_range(b, NULL, NULL));
+    CuAssertIntEquals(tc, 2, lighthouse_range(b));
     update_lighthouse(b);
     CuAssertIntEquals(tc, RF_LIGHTHOUSE, r1->flags&RF_LIGHTHOUSE);
     CuAssertPtrEquals(tc, NULL, r1->attribs);
diff --git a/src/reports.c b/src/reports.c
index 5d22636e8..711d09874 100644
--- a/src/reports.c
+++ b/src/reports.c
@@ -1596,7 +1596,7 @@ void prepare_report(report_context *ctx, faction *f)
                     for (b = rbuildings(r); b; b = b->next) {
                         if (b && b->type == bt_lighthouse) {
                             /* region owners get maximum range */
-                            int lhr = lighthouse_range(b, NULL, NULL);
+                            int lhr = lighthouse_view_distance(b, NULL);
                             if (lhr > range) range = lhr;
                         }
                     }
@@ -1613,7 +1613,7 @@ void prepare_report(report_context *ctx, faction *f)
                      */
                     if (!fval(r, RF_LIGHTHOUSE)) {
                         /* it's enough to add the region once, and if there are
-                        * no lighthouses, there is no need to look at more units */
+                         * no lighthouses here, there is no need to look at more units */
                         break;
                     }
                 }
@@ -1633,10 +1633,10 @@ void prepare_report(report_context *ctx, faction *f)
                         /* unit is one of ours, and inside the current lighthouse */
                         if (br == 0) {
                             /* lazy-calculate the range */
-                            br = lighthouse_range(b, f, u);
-                        }
-                        if (br > range) {
-                            range = br;
+                            br = lighthouse_view_distance(b, u);
+                            if (br > range) {
+                                range = br;
+                            }
                         }
                     }
                 }
diff --git a/src/reports.test.c b/src/reports.test.c
index 6d7a7961f..5a6e1fd9e 100644
--- a/src/reports.test.c
+++ b/src/reports.test.c
@@ -495,7 +495,7 @@ void test_prepare_lighthouse_capacity(CuTest *tc) {
     u1->number = 4;
     u1->building = b;
     set_level(u1, SK_PERCEPTION, 3);
-    CuAssertIntEquals(tc, 1, lighthouse_range(b, u1->faction, u1));
+    CuAssertIntEquals(tc, 1, lighthouse_view_distance(b, u1));
     CuAssertPtrEquals(tc, b, inside_building(u1));
     u2 = test_create_unit(f, r1);
     u2->building = b;
@@ -597,7 +597,7 @@ static void test_prepare_lighthouse_owners(CuTest *tc)
     u = test_create_unit(test_create_faction(NULL), r1);
     u->building = b;
     region_set_owner(b->region, f, 0);
-    CuAssertIntEquals(tc, 2, lighthouse_range(b, NULL, NULL));
+    CuAssertIntEquals(tc, 2, lighthouse_view_distance(b, NULL));
     prepare_report(&ctx, f);
     CuAssertPtrEquals(tc, r1, ctx.first);
     CuAssertPtrEquals(tc, NULL, ctx.last);

From e62b5c580dd283f2533f9049b316c2ee59a9e934 Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Thu, 19 Jul 2018 04:57:33 +0200
Subject: [PATCH 36/62] update tests to reflect new lighthouse rules

---
 tests/run-turn.sh      | 10 +++++-----
 tests/write-reports.sh |  2 +-
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/tests/run-turn.sh b/tests/run-turn.sh
index ad2e0e6b8..470cb6e2b 100755
--- a/tests/run-turn.sh
+++ b/tests/run-turn.sh
@@ -54,11 +54,11 @@ assert_grep_count reports/$CRFILE '^EINHEIT' 2
 assert_grep_count reports/$CRFILE '^GEGENSTAENDE' 2
 
 assert_grep_count reports/185-heg.cr '185;Runde' 1
-assert_grep_count reports/185-heg.cr ';Baeume' 4
-assert_grep_count reports/185-heg.cr '"B.ume";type' 4
-assert_grep_count reports/185-heg.cr '"Pferde";type' 6
-assert_grep_count reports/185-heg.nr 'erblickt' 6
-assert_grep_count reports/185-heg.cr '"lighthouse";visibility' 6
+assert_grep_count reports/185-heg.cr ';Baeume' 2
+assert_grep_count reports/185-heg.cr '"B.ume";type' 2
+assert_grep_count reports/185-heg.cr '"Pferde";type' 2
+assert_grep_count reports/185-heg.nr 'erblickt' 2
+assert_grep_count reports/185-heg.cr '"lighthouse";visibility' 2
 assert_grep_count reports/185-heg.cr '"neighbour";visibility' 11
 assert_grep_count reports/185-6rLo.cr '^EINHEIT' 2
 assert_grep_count reports/185-6rLo.cr '^REGION' 13
diff --git a/tests/write-reports.sh b/tests/write-reports.sh
index 5621e84a9..c3d1066e3 100755
--- a/tests/write-reports.sh
+++ b/tests/write-reports.sh
@@ -19,7 +19,7 @@ done
 #set -e
 cd $ROOT/tests
 setup
-#cleanup
+cleanup
 VALGRIND=`which valgrind`
 TESTS=../Debug/eressea/test_eressea
 SERVER=../Debug/eressea/eressea

From a4a3ebd633012d5977ee621fad00c953d661231e Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno@kn-bremen.de>
Date: Sat, 21 Jul 2018 21:41:13 +0200
Subject: [PATCH 37/62] backup the reports, because sometimes I am an idiot

---
 process/backup-eressea | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/process/backup-eressea b/process/backup-eressea
index 4914f5af0..4e5f69002 100755
--- a/process/backup-eressea
+++ b/process/backup-eressea
@@ -28,6 +28,9 @@ if [ -e orders.$TURN ]; then
 files="$files orders.$TURN"
 fi
 echo "backup turn $TURN, game $GAME, files: $files"
+if [ -d reports ] ; then
+  tar cjf backup/$TURN-reports.tar.bz2 reports
+fi
 tar cjf backup/$TURN.tar.bz2 $files
 echo "uploading game-$GAME/$TURN.tar.bz2"
 curl -s -n -T backup/$TURN.tar.bz2 https://dav.box.com/dav/Eressea/game-$GAME/$TURN.tar.bz2

From cd6154cae64acd5eaf996d653798daa413e7a915 Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Sun, 22 Jul 2018 10:55:09 +0200
Subject: [PATCH 38/62] fix the lighthouse crash in preview.

---
 scripts/eressea/xmasitems.lua | 2 --
 src/kernel/save.c             | 4 +++-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/scripts/eressea/xmasitems.lua b/scripts/eressea/xmasitems.lua
index 9e22dea52..a0f2e48f8 100644
--- a/scripts/eressea/xmasitems.lua
+++ b/scripts/eressea/xmasitems.lua
@@ -99,8 +99,6 @@ function self.update()
                     r:set_resource("tree", trees * 1.1)
                     msg:send_region(r)
                 end
-                if clear then
-                end
             end
         end
     else
diff --git a/src/kernel/save.c b/src/kernel/save.c
index b43d9bd5d..b51e82734 100644
--- a/src/kernel/save.c
+++ b/src/kernel/save.c
@@ -1493,7 +1493,9 @@ int read_game(gamedata *data)
         if (r->flags & RF_LIGHTHOUSE) {
             building *b;
             for (b = r->buildings; b; b = b->next) {
-                update_lighthouse(b);
+                if (is_lighthouse(b->type)) {
+                    update_lighthouse(b);
+                }
             }
         }
     }

From 11eb4980f84b9b26e60f7ec5530ab6ef0133b229 Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno@kn-bremen.de>
Date: Tue, 24 Jul 2018 13:57:46 +0200
Subject: [PATCH 39/62] use system-wide muttrc

---
 process/send-bz2-report | 2 +-
 process/send-zip-report | 2 +-
 s/preview               | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/process/send-bz2-report b/process/send-bz2-report
index c9fb87840..51a8113bf 100755
--- a/process/send-bz2-report
+++ b/process/send-bz2-report
@@ -25,6 +25,6 @@ addr=$1
 subj=$2
 shift 2
 
-mutt -F "$ERESSEA/etc/muttrc" -s "$subj" -a "$@" -- "$addr" \
+mutt -s "$subj" -a "$@" -- "$addr" \
 	< "$ERESSEA/server/etc/$TEMPLATE"
 
diff --git a/process/send-zip-report b/process/send-zip-report
index 89f742b82..a6bd85246 100755
--- a/process/send-zip-report
+++ b/process/send-zip-report
@@ -44,6 +44,6 @@ addr=$1
 subject=$2
 shift 2
 
-mutt -F "$ERESSEA/etc/muttrc" -s "$subject" -a "$@" -- "$addr" \
+mutt -s "$subject" -a "$@" -- "$addr" \
 	< "$TEMPLATE" || echo "Sending failed for email/report: $2/$3"
 
diff --git a/s/preview b/s/preview
index 68fe6e39f..39d5466d0 100755
--- a/s/preview
+++ b/s/preview
@@ -93,7 +93,7 @@ email=$(grep "faction=$1:" reports.txt | cut -d: -f2 | sed 's/email=//')
 echo "sending reports to $1 / $email"
 info=/dev/null
 [ -e ../email.txt ] && info=../email.txt
-cat $info | mutt -F $ERESSEA/etc/muttrc -s "Testauswertung Spiel $game Partei $1" -a $zip -- $email
+cat $info | mutt -s "Testauswertung Spiel $game Partei $1" -a $zip -- $email
 }
 
 game=0

From e7184add00e0d1e1de1629489bddd5537e88f023 Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno@kn-bremen.de>
Date: Tue, 24 Jul 2018 13:57:46 +0200
Subject: [PATCH 40/62] use system-wide muttrc

---
 process/send-bz2-report | 2 +-
 process/send-zip-report | 2 +-
 s/preview               | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/process/send-bz2-report b/process/send-bz2-report
index c9fb87840..51a8113bf 100755
--- a/process/send-bz2-report
+++ b/process/send-bz2-report
@@ -25,6 +25,6 @@ addr=$1
 subj=$2
 shift 2
 
-mutt -F "$ERESSEA/etc/muttrc" -s "$subj" -a "$@" -- "$addr" \
+mutt -s "$subj" -a "$@" -- "$addr" \
 	< "$ERESSEA/server/etc/$TEMPLATE"
 
diff --git a/process/send-zip-report b/process/send-zip-report
index 89f742b82..a6bd85246 100755
--- a/process/send-zip-report
+++ b/process/send-zip-report
@@ -44,6 +44,6 @@ addr=$1
 subject=$2
 shift 2
 
-mutt -F "$ERESSEA/etc/muttrc" -s "$subject" -a "$@" -- "$addr" \
+mutt -s "$subject" -a "$@" -- "$addr" \
 	< "$TEMPLATE" || echo "Sending failed for email/report: $2/$3"
 
diff --git a/s/preview b/s/preview
index 68fe6e39f..39d5466d0 100755
--- a/s/preview
+++ b/s/preview
@@ -93,7 +93,7 @@ email=$(grep "faction=$1:" reports.txt | cut -d: -f2 | sed 's/email=//')
 echo "sending reports to $1 / $email"
 info=/dev/null
 [ -e ../email.txt ] && info=../email.txt
-cat $info | mutt -F $ERESSEA/etc/muttrc -s "Testauswertung Spiel $game Partei $1" -a $zip -- $email
+cat $info | mutt -s "Testauswertung Spiel $game Partei $1" -a $zip -- $email
 }
 
 game=0

From 5309c84224f892728d7af645a040cb53c9664d8d Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Wed, 25 Jul 2018 19:42:48 +0200
Subject: [PATCH 41/62] BUG 2465 Kein Item-Teleport bei Lied der Verfuehrung.

---
 res/translations/messages.de.po |  2 +-
 src/spells.c                    | 40 +++++++++++++++++++++------------
 2 files changed, 27 insertions(+), 15 deletions(-)

diff --git a/res/translations/messages.de.po b/res/translations/messages.de.po
index 3413ffc2c..b87fe241a 100644
--- a/res/translations/messages.de.po
+++ b/res/translations/messages.de.po
@@ -2121,7 +2121,7 @@ msgid "missing_components"
 msgstr "\"$unit($unit) hat nicht genügend Komponenten um $spell($spell) auf Stufe $int($level) zu zaubern.\""
 
 msgid "seduce_effect_1"
-msgstr "\"$unit($unit) verfiel dem Glücksspiel und hat fast sein ganzes Hab und gut verspielt.\""
+msgstr "\"$unit($unit) verfiel dem Glücksspiel und hat fast sein ganzes Hab und Gut verspielt.\""
 
 msgid "xmastree_effect"
 msgstr "\"In der Region erstrahlen des Nachts bunte Lichter, Gloeckchen klingeln und frohes Kindergelaechter klingt durch den Wald.\""
diff --git a/src/spells.c b/src/spells.c
index 4e98b1a2a..8caf29039 100644
--- a/src/spells.c
+++ b/src/spells.c
@@ -4072,7 +4072,7 @@ static int sp_pump(castorder * co)
  *  Betoert eine Einheit, so das sie ihm den groe�ten Teil ihres Bargelds
  *  und 50% ihres Besitzes schenkt. Sie behaelt jedoch immer soviel, wie
  *  sie zum ueberleben braucht. Wirkt gegen Magieresistenz.
- *  MIN(Stufe*1000$, u->money - maintenace)
+ *  MIN(Stufe*1000$, u->money - maintenance)
  *  Von jedem Item wird 50% abgerundet ermittelt und uebergeben. Dazu
  *  kommt Itemzahl%2 mit 50% chance
  *
@@ -4083,15 +4083,16 @@ static int sp_seduce(castorder * co)
 {
     const resource_type *rsilver = get_resourcetype(R_SILVER);
     unit *target;
-    item **itmp, *items = 0;
-    unit *mage = co->magician.u;
+    item **itmp, *items = NULL;
+    unit *u, *mage = co->magician.u;
     spellparameter *pa = co->par;
     int cast_level = co->level;
     double force = co->force;
 
     /* wenn kein Ziel gefunden, Zauber abbrechen */
-    if (pa->param[0]->flag == TARGET_NOTFOUND)
+    if (pa->param[0]->flag == TARGET_NOTFOUND) {
         return 0;
+    }
 
     target = pa->param[0]->data.u;        /* Zieleinheit */
 
@@ -4101,6 +4102,15 @@ static int sp_seduce(castorder * co)
         return 0;
     }
 
+    u = mage;
+    if (mage->region != target->region) {
+        for (u = target->region->units; u; u = u->next) {
+            if (u->faction == mage->faction) {
+                break;
+            }
+        }
+    }
+
     /* Erfolgsmeldung */
 
     itmp = &target->items;
@@ -4113,28 +4123,30 @@ static int sp_seduce(castorder * co)
             if (loot < 0) loot = 0;
         }
         else {
-            loot = itm->number / 2;
-            if (itm->number % 2) {
-                loot += rng_int() % 2;
-            }
+            loot = (itm->number + 1) / 2;
             if (loot > 0) {
                 int floot = (int)(5 * force);
                 if (loot > floot) loot = floot;
             }
         }
         if (loot > 0) {
-            i_change(&mage->items, itm->type, loot);
-            i_change(&items, itm->type, loot);
+            if (u) {
+                i_change(&u->items, itm->type, loot);
+                i_change(&items, itm->type, loot);
+            }
             i_change(itmp, itm->type, -loot);
         }
-        if (*itmp == itm)
+        if (*itmp == itm) {
             itmp = &itm->next;
+        }
     }
 
     if (items) {
-        ADDMSG(&mage->faction->msgs, msg_message("seduce_effect_0", "mage unit items",
-            mage, target, items));
-        i_freeall(&items);
+        if (u) {
+            ADDMSG(&mage->faction->msgs, msg_message("seduce_effect_0", "mage unit items",
+                u, target, items));
+            i_freeall(&items);
+        }
         ADDMSG(&target->faction->msgs, msg_message("seduce_effect_1", "unit",
             target));
     }

From 7eb951544a1d0b9c1fbdb66cd1eccf259f0fc1ed Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Mon, 30 Jul 2018 22:19:40 +0200
Subject: [PATCH 42/62] equip_newunits got called twice.

---
 scripts/eressea/equipment.lua |  1 -
 scripts/tests/config.lua      | 21 +++++++++++++++++++++
 2 files changed, 21 insertions(+), 1 deletion(-)

diff --git a/scripts/eressea/equipment.lua b/scripts/eressea/equipment.lua
index 8852dfb27..c7bfe7dad 100644
--- a/scripts/eressea/equipment.lua
+++ b/scripts/eressea/equipment.lua
@@ -2,7 +2,6 @@
 local self = {}
 
 local function equip_first(u)
-    equip_newunits(u)
     name = 'seed_' .. u.race
     equip_unit(u, name, 255)
 end
diff --git a/scripts/tests/config.lua b/scripts/tests/config.lua
index 316a8ccca..733a8ee07 100644
--- a/scripts/tests/config.lua
+++ b/scripts/tests/config.lua
@@ -30,6 +30,26 @@ function test_first_troll()
     assert_equal(2, u:eff_skill('perception'))
 end
 
+function test_first_human()
+    local f = faction.create('human')
+    local r = region.create(0, 0, "plain")
+    local u = unit.create(f, r, 1)
+    u:equip('first_unit')
+    assert_not_nil(u.building)
+    assert_equal('castle', u.building.type)
+    assert_equal(10, u.building.size)
+end
+
+function test_first_aquarian()
+    local f = faction.create('aquarian')
+    local r = region.create(0, 0, "plain")
+    local u = unit.create(f, r, 1)
+    u:equip('first_unit')
+    assert_not_nil(u.ship)
+    assert_equal('boat', u.ship.type)
+    assert_equal(1, u:get_skill('sailing'))
+end
+
 function test_seed_unit()
     local r = region.create(0, 0, "plain")
     local f = faction.create('human')
@@ -53,3 +73,4 @@ function test_seed_elf()
     assert_equal('castle', u.building.type)
     assert_equal(10, u.building.size)
 end
+

From ec571924037cc06b23eff622b828f865cd65a57b Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Mon, 30 Jul 2018 22:19:40 +0200
Subject: [PATCH 43/62] equip_newunits got called twice.

---
 scripts/eressea/equipment.lua |  1 -
 scripts/tests/config.lua      | 21 +++++++++++++++++++++
 2 files changed, 21 insertions(+), 1 deletion(-)

diff --git a/scripts/eressea/equipment.lua b/scripts/eressea/equipment.lua
index 8852dfb27..c7bfe7dad 100644
--- a/scripts/eressea/equipment.lua
+++ b/scripts/eressea/equipment.lua
@@ -2,7 +2,6 @@
 local self = {}
 
 local function equip_first(u)
-    equip_newunits(u)
     name = 'seed_' .. u.race
     equip_unit(u, name, 255)
 end
diff --git a/scripts/tests/config.lua b/scripts/tests/config.lua
index 316a8ccca..733a8ee07 100644
--- a/scripts/tests/config.lua
+++ b/scripts/tests/config.lua
@@ -30,6 +30,26 @@ function test_first_troll()
     assert_equal(2, u:eff_skill('perception'))
 end
 
+function test_first_human()
+    local f = faction.create('human')
+    local r = region.create(0, 0, "plain")
+    local u = unit.create(f, r, 1)
+    u:equip('first_unit')
+    assert_not_nil(u.building)
+    assert_equal('castle', u.building.type)
+    assert_equal(10, u.building.size)
+end
+
+function test_first_aquarian()
+    local f = faction.create('aquarian')
+    local r = region.create(0, 0, "plain")
+    local u = unit.create(f, r, 1)
+    u:equip('first_unit')
+    assert_not_nil(u.ship)
+    assert_equal('boat', u.ship.type)
+    assert_equal(1, u:get_skill('sailing'))
+end
+
 function test_seed_unit()
     local r = region.create(0, 0, "plain")
     local f = faction.create('human')
@@ -53,3 +73,4 @@ function test_seed_elf()
     assert_equal('castle', u.building.type)
     assert_equal(10, u.building.size)
 end
+

From ad0464ab50d1615b71a7acd857e68252939196cd Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Wed, 1 Aug 2018 10:04:12 +0200
Subject: [PATCH 44/62] BUG 2168: call sink_ship for ships that take too much
 damage.

---
 clibs                           |  2 +-
 scripts/tests/e2/e2features.lua | 13 +++++++++++++
 src/battle.c                    |  5 ++++-
 src/bind_unit.c                 |  2 +-
 src/chaos.c                     | 20 +++++++++++---------
 src/chaos.h                     |  2 --
 src/kernel/ship.h               |  6 +++---
 src/laws.c                      |  1 +
 src/move.c                      |  9 +++++++--
 src/randenc.c                   |  3 +++
 src/spy.c                       | 25 +++++++++++++++----------
 src/spy.h                       |  6 ++----
 12 files changed, 61 insertions(+), 33 deletions(-)

diff --git a/clibs b/clibs
index 9b6e34959..d86c85254 160000
--- a/clibs
+++ b/clibs
@@ -1 +1 @@
-Subproject commit 9b6e34959f77d7ca3a4ce3826cb487487f557441
+Subproject commit d86c8525489d7f11b7ba13e101bb59ecf160b871
diff --git a/scripts/tests/e2/e2features.lua b/scripts/tests/e2/e2features.lua
index be09ec448..721ad7a1c 100644
--- a/scripts/tests/e2/e2features.lua
+++ b/scripts/tests/e2/e2features.lua
@@ -517,3 +517,16 @@ function test_buy_sell()
     assert_equal(4, u:get_item(item))
     assert_not_equal(0, u:get_item('money'))
 end
+
+function test_seaserpent_attack()
+    local r = region.create(0, 0, 'ocean')
+    local sh = ship.create(r, 'boat')
+    local us = unit.create(get_monsters(), r, 1, 'seaserpent')
+    local u = unit.create(faction.create('human'), r, 20, 'human')
+    u.ship = sh
+    us:clear_orders()
+    us:add_order('ATTACKIERE ' .. itoa36(u.id))
+    us:set_skill('unarmed', 10)
+    process_orders()
+    write_reports()
+end
diff --git a/src/battle.c b/src/battle.c
index 38590b8a6..9e2f841c0 100644
--- a/src/battle.c
+++ b/src/battle.c
@@ -26,6 +26,7 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 #include "move.h"
 #include "skill.h"
 #include "study.h"
+#include "spy.h"
 
 #include <spells/buildingcurse.h>
 #include <spells/regioncurse.h>
@@ -2787,10 +2788,12 @@ static void aftermath(battle * b)
             ship *sh = *sp;
             freset(sh, SF_DAMAGED);
             if (sh->damage >= sh->size * DAMAGE_SCALE) {
+                sink_ship(sh);
                 remove_ship(sp, sh);
             }
-            if (*sp == sh)
+            else {
                 sp = &sh->next;
+            }
         }
     }
 
diff --git a/src/bind_unit.c b/src/bind_unit.c
index e0fa9f86b..7858aadf2 100644
--- a/src/bind_unit.c
+++ b/src/bind_unit.c
@@ -909,8 +909,8 @@ static int tolua_unit_create(lua_State * L)
     faction *f = (faction *)tolua_tousertype(L, 1, 0);
     region *r = (region *)tolua_tousertype(L, 2, 0);
     unit *u;
-    const char *rcname = tolua_tostring(L, 4, NULL);
     int num = (int)tolua_tonumber(L, 3, 1);
+    const char *rcname = tolua_tostring(L, 4, NULL);
     const race *rc;
 
     assert(f && r);
diff --git a/src/chaos.c b/src/chaos.c
index 21a5a8624..1ec431b7e 100644
--- a/src/chaos.c
+++ b/src/chaos.c
@@ -21,6 +21,7 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 #include "chaos.h"
 #include "monsters.h"
 #include "move.h"
+#include "spy.h"
 
 #include <kernel/building.h>
 #include <kernel/faction.h>
@@ -144,19 +145,20 @@ static void chaos(region * r)
                             break;
                     }
                     if (dir != MAXDIRECTIONS) {
-                        ship *sh = r->ships;
+                        ship **slist = &r->ships;
                         unit **up;
 
-                        while (sh) {
-                            ship *nsh = sh->next;
-                            double dmg =
-                                config_get_flt("rules.ship.damage.atlantis",
-                                0.50);
-                            damage_ship(sh, dmg);
+                        while (*slist) {
+                            ship *sh = *slist;
+
+                            damage_ship(sh, 0.5);
                             if (sh->damage >= sh->size * DAMAGE_SCALE) {
-                                remove_ship(&sh->region->ships, sh);
+                                sink_ship(sh);
+                                remove_ship(slist, sh);
+                            }
+                            else {
+                                slist = &sh->next;
                             }
-                            sh = nsh;
                         }
 
                         for (up = &r->units; *up;) {
diff --git a/src/chaos.h b/src/chaos.h
index 550aa3efb..cb913d1a0 100644
--- a/src/chaos.h
+++ b/src/chaos.h
@@ -22,8 +22,6 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 extern "C" {
 #endif
 
-    struct region;
-
     void chaos_update(void);
 
 #ifdef __cplusplus
diff --git a/src/kernel/ship.h b/src/kernel/ship.h
index c726b4839..ce3283a6f 100644
--- a/src/kernel/ship.h
+++ b/src/kernel/ship.h
@@ -124,9 +124,9 @@ extern "C" {
     extern void write_ship_reference(const struct ship *sh,
     struct storage *store);
 
-    extern void remove_ship(struct ship **slist, struct ship *s);
-    extern void free_ship(struct ship *s);
-    extern void free_ships(void);
+    void remove_ship(struct ship **slist, struct ship *s);
+    void free_ship(struct ship *s);
+    void free_ships(void);
 
     const char *ship_getname(const struct ship *sh);
     void ship_setname(struct ship *self, const char *name);
diff --git a/src/laws.c b/src/laws.c
index aa6e237e2..35e4732f1 100644
--- a/src/laws.c
+++ b/src/laws.c
@@ -2590,6 +2590,7 @@ void sinkships(struct region * r)
             }
         }
         if (sh->damage >= sh->size * DAMAGE_SCALE) {
+            sink_ship(sh);
             remove_ship(shp, sh);
         }
         if (*shp == sh)
diff --git a/src/move.c b/src/move.c
index 205b52416..b815f5ea5 100644
--- a/src/move.c
+++ b/src/move.c
@@ -47,6 +47,7 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 #include "laws.h"
 #include "reports.h"
 #include "study.h"
+#include "spy.h"
 #include "alchemy.h"
 #include "travelthru.h"
 #include "vortex.h"
@@ -541,6 +542,7 @@ static ship *do_maelstrom(region * r, unit * u)
     if (sh->damage >= sh->size * DAMAGE_SCALE) {
         ADDMSG(&u->faction->msgs, msg_message("entermaelstrom",
             "region ship damage sink", r, sh, damage, 1));
+        sink_ship(sh);
         remove_ship(&sh->region->ships, sh);
         return NULL;
     }
@@ -882,12 +884,14 @@ static void drifting_ships(region * r)
                 }
                 if (sh->damage >= sh->size * DAMAGE_SCALE) {
                     msg_to_ship_inmates(sh, &firstu, &lastu, msg_message("shipsink", "ship", sh));
-                    remove_ship(&sh->region->ships, sh);
+                    sink_ship(sh);
+                    remove_ship(shp, sh);
                 }
             }
 
-            if (*shp == sh)
+            if (*shp == sh) {
                 shp = &sh->next;
+            }
         }
     }
 }
@@ -1926,6 +1930,7 @@ static void sail(unit * u, order * ord, region_list ** routep, bool drifting)
     if (sh->damage >= sh->size * DAMAGE_SCALE) {
         if (sh->region) {
             ADDMSG(&f->msgs, msg_message("shipsink", "ship", sh));
+            sink_ship(sh);
             remove_ship(&sh->region->ships, sh);
         }
         sh = NULL;
diff --git a/src/randenc.c b/src/randenc.c
index 7de49b9e2..820d3760f 100644
--- a/src/randenc.c
+++ b/src/randenc.c
@@ -26,6 +26,7 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 #include "economy.h"
 #include "monsters.h"
 #include "move.h"
+#include "spy.h"
 #include "study.h"
 #include "volcano.h"
 
@@ -295,6 +296,7 @@ static void move_iceberg(region * r)
                             ADDMSG(&u->faction->msgs, msg_message("overrun_by_iceberg_des",
                                 "ship", sh));
                         }
+                        sink_ship(sh);
                         remove_ship(&sh->region->ships, sh);
                     }
                     else if (u != NULL) {
@@ -421,6 +423,7 @@ static void godcurse(void)
                             ADDMSG(&uo->faction->msgs,
                                 msg_message("godcurse_destroy_ship", "ship", sh));
                         }
+                        sink_ship(sh);
                         remove_ship(&sh->region->ships, sh);
                     }
                     sh = shn;
diff --git a/src/spy.c b/src/spy.c
index 94d6c4e7d..c0afbe43c 100644
--- a/src/spy.c
+++ b/src/spy.c
@@ -392,19 +392,22 @@ static int try_destruction(unit * u, unit * u2, const ship * sh, int skilldiff)
     return 1;                     /* success */
 }
 
-static void sink_ship(region * r, ship * sh, unit * saboteur)
+#define OCEAN_SWIMMER_CHANCE 0.1
+#define COAST_SWIMMER_CHANCE 0.9
+
+void sink_ship(ship * sh)
 {
     unit **ui, *u;
-    region *safety = r;
+    region *r, *safety;
     int i;
     direction_t d;
     double probability = 0.0;
     message *sink_msg = NULL;
     faction *f;
 
-    assert(r);
-    assert(sh);
-    assert(saboteur);
+    assert(sh && sh->region);
+    r = sh->region;
+
     for (f = NULL, u = r->units; u; u = u->next) {
         /* slight optimization to avoid dereferencing u->faction each time */
         if (f != u->faction) {
@@ -414,8 +417,9 @@ static void sink_ship(region * r, ship * sh, unit * saboteur)
     }
 
     /* figure out what a unit's chances of survival are: */
+    safety = r;
     if (!(r->terrain->flags & SEA_REGION)) {
-        probability = CANAL_SWIMMER_CHANCE;
+        probability = COAST_SWIMMER_CHANCE;
     }
     else {
         for (d = 0; d != MAXDIRECTIONS; ++d) {
@@ -479,10 +483,9 @@ static void sink_ship(region * r, ship * sh, unit * saboteur)
         }
         ui = &u->next;
     }
-    if (sink_msg)
+    if (sink_msg) {
         msg_release(sink_msg);
-    /* finally, get rid of the ship */
-    remove_ship(&sh->region->ships, sh);
+    }
 }
 
 int sabotage_cmd(unit * u, struct order *ord)
@@ -514,7 +517,9 @@ int sabotage_cmd(unit * u, struct order *ord)
                 effskill(u, SK_SPY, 0) - top_skill(u->region, u2->faction, sh, SK_PERCEPTION);
         }
         if (try_destruction(u, u2, sh, skdiff)) {
-            sink_ship(u->region, sh, u);
+            sink_ship(sh);
+            /* finally, get rid of the ship */
+            remove_ship(&sh->region->ships, sh);
         }
         break;
     default:
diff --git a/src/spy.h b/src/spy.h
index 665595ef4..b0bd7ae0e 100644
--- a/src/spy.h
+++ b/src/spy.h
@@ -27,6 +27,7 @@ extern "C" {
     struct strlist;
     struct order;
     struct faction;
+    struct ship;
 
     int setstealth_cmd(struct unit *u, struct order *ord);
     int spy_cmd(struct unit *u, struct order *ord);
@@ -34,10 +35,7 @@ extern "C" {
     void spy_message(int spy, const struct unit *u,
         const struct unit *target);
     void set_factionstealth(struct unit * u, struct faction * f);
-    
-
-#define OCEAN_SWIMMER_CHANCE 0.1
-#define CANAL_SWIMMER_CHANCE 0.9
+    void sink_ship(struct ship * sh);
 
 #ifdef __cplusplus
 }

From e89fe69d58518d76f1b28627655a5a9cbaabbfa8 Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Wed, 1 Aug 2018 11:55:57 +0200
Subject: [PATCH 45/62] simplicate the sink_ship function, do not rescue units.

---
 res/core/messages.xml           | 15 ------
 res/translations/messages.de.po |  6 ---
 res/translations/messages.en.po |  6 ---
 scripts/tests/e2/e2features.lua |  2 +-
 src/move.test.c                 |  3 ++
 src/spy.c                       | 83 ++++++---------------------------
 src/spy.test.c                  | 54 ++++++++++++++++-----
 7 files changed, 60 insertions(+), 109 deletions(-)

diff --git a/res/core/messages.xml b/res/core/messages.xml
index d566c82e6..0cd66ca08 100644
--- a/res/core/messages.xml
+++ b/res/core/messages.xml
@@ -868,21 +868,6 @@
     </type>
   </message>
 
-  <message name="sink_lost_msg" section="events">
-    <type>
-      <arg name="dead" type="int"/>
-      <arg name="region" type="region"/>
-      <arg name="unit" type="unit"/>
-    </type>
-  </message>
-
-  <message name="sink_saved_msg" section="events">
-    <type>
-      <arg name="region" type="region"/>
-      <arg name="unit" type="unit"/>
-    </type>
-  </message>
-
   <message name="sink_msg" section="events">
     <type>
       <arg name="ship" type="ship"/>
diff --git a/res/translations/messages.de.po b/res/translations/messages.de.po
index b87fe241a..1d45faaff 100644
--- a/res/translations/messages.de.po
+++ b/res/translations/messages.de.po
@@ -2303,9 +2303,6 @@ msgstr "\"$unit($mage) erleidet durch den Tod seines Vertrauten einen Schock.\""
 msgid "error269"
 msgstr "\"$unit($unit) in $region($region): '$order($command)' - Hier kann man nicht zaubern.\""
 
-msgid "sink_saved_msg"
-msgstr "\"$unit($unit) überlebt unbeschadet und rettet sich nach $region($region).\""
-
 msgid "race_noregroup"
 msgstr "\"$unit($unit) in $region($region): '$order($command)' - $race($race,0) können nicht neu gruppiert werden.\""
 
@@ -2654,9 +2651,6 @@ msgstr "\"$unit($unit) in $region($region): '$order($command)' - Die Einheit kan
 msgid "analyse_building_noage"
 msgstr "\"$unit($mage) fand heraus, dass auf $building($building) der Zauber '$curse($curse)' liegt, dessen Kraft ausreicht, um noch Jahrhunderte bestehen zu bleiben.\""
 
-msgid "sink_lost_msg"
-msgstr "\"$int($amount) Personen von $unit($unit) ertrinken.$if($isnull($region),\"\",\" Die Einheit rettet sich nach $region($region).\")\""
-
 msgid "error130"
 msgstr "\"$unit($unit) in $region($region): '$order($command)' - Syntax: MAGIEGEBIET [1-5].\""
 
diff --git a/res/translations/messages.en.po b/res/translations/messages.en.po
index 6eaf6f258..bf9d8e3c9 100644
--- a/res/translations/messages.en.po
+++ b/res/translations/messages.en.po
@@ -2303,9 +2303,6 @@ msgstr "\"$unit($mage) receives a shock when his familiar dies.\""
 msgid "error269"
 msgstr "\"$unit($unit) in $region($region): '$order($command)' - You cannot cast spells here.\""
 
-msgid "sink_saved_msg"
-msgstr "\"$unit($unit) survives unscathed and makes it to $region($region).\""
-
 msgid "race_noregroup"
 msgstr "\"$unit($unit) in $region($region): '$order($command)' - $race($race,0) cannot be regrouped.\""
 
@@ -2654,9 +2651,6 @@ msgstr "\"$unit($unit) in $region($region): '$order($command)' - The unit cannot
 msgid "analyse_building_noage"
 msgstr "\"$unit($mage) discovers that $building($building) is charmed with '$curse($curse)', which will last for centuries.\""
 
-msgid "sink_lost_msg"
-msgstr "\"$int($amount) people of $unit($unit) drown.$if($isnull($region),\"\",\" The unit makes it to $region($region).\")\""
-
 msgid "error130"
 msgstr "\"$unit($unit) in $region($region): '$order($command)' - Syntax: MAGIC SPHERE [1-5].\""
 
diff --git a/scripts/tests/e2/e2features.lua b/scripts/tests/e2/e2features.lua
index 721ad7a1c..bf90ab6a0 100644
--- a/scripts/tests/e2/e2features.lua
+++ b/scripts/tests/e2/e2features.lua
@@ -522,7 +522,7 @@ function test_seaserpent_attack()
     local r = region.create(0, 0, 'ocean')
     local sh = ship.create(r, 'boat')
     local us = unit.create(get_monsters(), r, 1, 'seaserpent')
-    local u = unit.create(faction.create('human'), r, 20, 'human')
+    local u = unit.create(faction.create('human', 'enno@example.com'), r, 20, 'human')
     u.ship = sh
     us:clear_orders()
     us:add_order('ATTACKIERE ' .. itoa36(u.id))
diff --git a/src/move.test.c b/src/move.test.c
index f8c11114f..f01915e2a 100644
--- a/src/move.test.c
+++ b/src/move.test.c
@@ -287,6 +287,9 @@ void setup_drift (struct drift_fixture *fix) {
     u_set_ship(fix->u, fix->sh = test_create_ship(fix->u->region, fix->st_boat));
     assert(fix->sh);
 
+    mt_create_va(mt_new("sink_msg", NULL),
+        "ship:ship", "region:region", MT_NEW_END);
+
     mt_create_va(mt_new("ship_drift", NULL),
         "ship:ship", "dir:int", MT_NEW_END);
     mt_create_va(mt_new("shipsink", NULL),
diff --git a/src/spy.c b/src/spy.c
index c0afbe43c..ae70f4db8 100644
--- a/src/spy.c
+++ b/src/spy.c
@@ -392,16 +392,10 @@ static int try_destruction(unit * u, unit * u2, const ship * sh, int skilldiff)
     return 1;                     /* success */
 }
 
-#define OCEAN_SWIMMER_CHANCE 0.1
-#define COAST_SWIMMER_CHANCE 0.9
-
 void sink_ship(ship * sh)
 {
-    unit **ui, *u;
-    region *r, *safety;
-    int i;
-    direction_t d;
-    double probability = 0.0;
+    unit *u;
+    region *r;
     message *sink_msg = NULL;
     faction *f;
 
@@ -416,72 +410,23 @@ void sink_ship(ship * sh)
         }
     }
 
-    /* figure out what a unit's chances of survival are: */
-    safety = r;
-    if (!(r->terrain->flags & SEA_REGION)) {
-        probability = COAST_SWIMMER_CHANCE;
-    }
-    else {
-        for (d = 0; d != MAXDIRECTIONS; ++d) {
-            region *rn = rconnect(r, d);
-            if (rn && !(rn->terrain->flags & SEA_REGION) && !move_blocked(NULL, r, rn)) {
-                safety = rn;
-                probability = OCEAN_SWIMMER_CHANCE;
-                break;
-            }
-        }
-    }
-    for (ui = &r->units; *ui;) {
+    for (f = NULL, u = r->units; u; u = u->next) {
         /* inform this faction about the sinking ship: */
-        u = *ui;
-        if (!(u->faction->flags & FFL_SELECT)) {
-            fset(u->faction, FFL_SELECT);
-            if (sink_msg == NULL) {
-                sink_msg = msg_message("sink_msg", "ship region", sh, r);
-            }
-            add_message(&f->msgs, sink_msg);
-        }
-
         if (u->ship == sh) {
-            int dead = 0;
-            message *msg;
-
-            /* if this fails, I misunderstood something: */
-            for (i = 0; i != u->number; ++i)
-                if (chance(probability))
-                    ++dead;
-
-            if (dead != u->number) {
-                /* she will live. but her items get stripped */
-                if (dead > 0) {
-                    msg =
-                        msg_message("sink_lost_msg", "dead region unit", dead, safety, u);
-                }
-                else {
-                    msg = msg_message("sink_saved_msg", "region unit", safety, u);
-                }
-                leave_ship(u);
-                if (r != safety) {
-                    setguard(u, false);
-                }
-                while (u->items) {
-                    i_remove(&u->items, u->items);
-                }
-                move_unit(u, safety, NULL);
-            }
-            else {
-                msg = msg_message("sink_lost_msg", "dead region unit", dead, (region *)NULL, u);
-            }
-            add_message(&u->faction->msgs, msg);
-            msg_release(msg);
-            if (dead == u->number) {
-                if (remove_unit(ui, u) == 0) {
-                    /* ui is already pointing at u->next */
-                    continue;
+            if (f != u->faction) {
+                f = u->faction;
+                if (!(f->flags & FFL_SELECT)) {
+                    f->flags |= FFL_SELECT;
+                    if (sink_msg == NULL) {
+                        sink_msg = msg_message("sink_msg", "ship region", sh, r);
+                    }
+                    add_message(&f->msgs, sink_msg);
                 }
             }
         }
-        ui = &u->next;
+        else if (f != NULL) {
+            break;
+        }
     }
     if (sink_msg) {
         msg_release(sink_msg);
diff --git a/src/spy.test.c b/src/spy.test.c
index ed10c4b7e..0224698a0 100644
--- a/src/spy.test.c
+++ b/src/spy.test.c
@@ -55,10 +55,6 @@ static void setup_spy(spy_fixture *fix) {
         "ship:ship", MT_NEW_END);
     mt_create_va(mt_new("sink_msg", NULL), 
         "ship:ship", "region:region", MT_NEW_END);
-    mt_create_va(mt_new("sink_lost_msg", NULL), 
-        "unit:unit", "region:region", "dead:int", MT_NEW_END);
-    mt_create_va(mt_new("sink_saved_msg", NULL),
-        "unit:unit", "region:region", MT_NEW_END);
 
     if (fix) {
         fix->r = test_create_region(0, 0, NULL);
@@ -112,6 +108,7 @@ static void test_sabotage_self(CuTest *tc) {
     unit *u;
     region *r;
     order *ord;
+    message *msg;
 
     test_setup();
     setup_spy(NULL);
@@ -119,17 +116,49 @@ static void test_sabotage_self(CuTest *tc) {
     assert(r);
     u = test_create_unit(test_create_faction(NULL), r);
     assert(u && u->faction && u->region == r);
-    u->ship = test_create_ship(r, test_create_shiptype("boat"));
+    u->ship = test_create_ship(r, NULL);
     assert(u->ship);
     ord = create_order(K_SABOTAGE, u->faction->locale, "SCHIFF");
     assert(ord);
     CuAssertIntEquals(tc, 0, sabotage_cmd(u, ord));
-    CuAssertPtrEquals(tc, 0, r->ships);
-    CuAssertPtrNotNull(tc, test_find_messagetype(u->faction->msgs, "sink_msg"));
+    CuAssertPtrEquals(tc, NULL, r->ships);
+    CuAssertPtrNotNull(tc, msg = test_find_messagetype(u->faction->msgs, "sink_msg"));
+    CuAssertPtrEquals(tc, NULL, test_find_messagetype_ex(u->faction->msgs, "sink_msg", msg));
     free_order(ord);
     test_teardown();
 }
 
+static void test_sink_ship(CuTest *tc) {
+    ship *sh;
+    unit *u1, *u2, *u3;
+    region *r;
+    message *msg;
+
+    test_setup();
+    setup_spy(NULL);
+    r = test_create_ocean(0, 0);
+    u1 = test_create_unit(test_create_faction(NULL), r);
+    u2 = test_create_unit(u1->faction, r);
+    u3 = test_create_unit(test_create_faction(NULL), r);
+    u1->ship = u2->ship = u3->ship = sh = test_create_ship(r, NULL);
+
+    sink_ship(sh);
+    CuAssertPtrEquals(tc, r, sh->region);
+    CuAssertPtrEquals(tc, sh, r->ships);
+    CuAssertPtrNotNull(tc, msg = test_find_messagetype(u1->faction->msgs, "sink_msg"));
+    CuAssertPtrEquals(tc, NULL, test_find_messagetype_ex(u1->faction->msgs, "sink_msg", msg));
+    CuAssertPtrNotNull(tc, msg = test_find_messagetype(u3->faction->msgs, "sink_msg"));
+    CuAssertPtrEquals(tc, NULL, test_find_messagetype_ex(u3->faction->msgs, "sink_msg", msg));
+
+    remove_ship(&r->ships, sh);
+    CuAssertPtrEquals(tc, NULL, sh->region);
+    CuAssertPtrEquals(tc, NULL, r->ships);
+    CuAssertPtrEquals(tc, NULL, u1->ship);
+    CuAssertPtrEquals(tc, NULL, u2->ship);
+    CuAssertPtrEquals(tc, NULL, u3->ship);
+
+    test_teardown();
+}
 
 static void test_sabotage_other_fail(CuTest *tc) {
     unit *u, *u2;
@@ -145,7 +174,7 @@ static void test_sabotage_other_fail(CuTest *tc) {
     u = test_create_unit(test_create_faction(NULL), r);
     u2 = test_create_unit(test_create_faction(NULL), r);
     assert(u && u2);
-    u2->ship = test_create_ship(r, test_create_shiptype("boat"));
+    u2->ship = test_create_ship(r, NULL);
     assert(u2->ship);
     u->ship = u2->ship;
     ship_update_owner(u->ship);
@@ -167,7 +196,7 @@ static void test_setstealth_cmd(CuTest *tc) {
     const struct locale *lang;
 
     test_setup();
-    u = test_create_unit(test_create_faction(NULL), test_create_region(0, 0, NULL));
+    u = test_create_unit(test_create_faction(NULL), test_create_plain(0, 0));
     lang = u->faction->locale;
     u->flags = UFL_ANON_FACTION | UFL_SIEGE;
     u->thisorder = create_order(K_SETSTEALTH, lang, "%s %s",
@@ -191,7 +220,7 @@ static void test_setstealth_demon(CuTest *tc) {
     test_setup();
     lang = test_create_locale();
     rc = test_create_race("demon");
-    u = test_create_unit(test_create_faction(rc), test_create_region(0, 0, NULL));
+    u = test_create_unit(test_create_faction(rc), test_create_plain(0, 0));
     rc = test_create_race("dwarf");
     init_races(lang);
     u->thisorder = create_order(K_SETSTEALTH, lang, racename(lang, u, rc));
@@ -208,7 +237,7 @@ static void test_setstealth_demon_bad(CuTest *tc) {
     test_setup();
     lang = test_create_locale();
     rc = test_create_race("demon");
-    u = test_create_unit(test_create_faction(rc), test_create_region(0, 0, NULL));
+    u = test_create_unit(test_create_faction(rc), test_create_plain(0, 0));
 
     rc = test_create_race("smurf");
     rc->flags &= ~RCF_PLAYABLE;
@@ -232,7 +261,7 @@ static void test_sabotage_other_success(CuTest *tc) {
     u = test_create_unit(test_create_faction(NULL), r);
     u2 = test_create_unit(test_create_faction(NULL), r);
     assert(u && u2);
-    u2->ship = test_create_ship(r, test_create_shiptype("boat"));
+    u2->ship = test_create_ship(r, NULL);
     assert(u2->ship);
     u->ship = u2->ship;
     ship_update_owner(u->ship);
@@ -251,6 +280,7 @@ CuSuite *get_spy_suite(void)
     CuSuite *suite = CuSuiteNew();
     SUITE_ADD_TEST(suite, test_simple_spy_message);
     SUITE_ADD_TEST(suite, test_all_spy_message);
+    SUITE_ADD_TEST(suite, test_sink_ship);
     SUITE_ADD_TEST(suite, test_sabotage_self);
     SUITE_ADD_TEST(suite, test_setstealth_cmd);
     SUITE_ADD_TEST(suite, test_setstealth_demon);

From 3880960acf47510d63a0a9a9b93cf470e3c1848e Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Thu, 2 Aug 2018 13:53:41 +0200
Subject: [PATCH 46/62] BUG 2458: fix insect recruit warnings.

---
 res/translations/messages.de.po |  4 +--
 src/economy.c                   |  2 +-
 src/reports.c                   | 27 +++++++++----------
 src/reports.h                   |  2 +-
 src/reports.test.c              | 47 +++++++++++++++++++++++++--------
 5 files changed, 52 insertions(+), 30 deletions(-)

diff --git a/res/translations/messages.de.po b/res/translations/messages.de.po
index 1d45faaff..507f0e39d 100644
--- a/res/translations/messages.de.po
+++ b/res/translations/messages.de.po
@@ -1773,7 +1773,7 @@ msgid "storm"
 msgstr "\"Die $ship($ship) wird in $region($region) von Stürmen abgetrieben$if($sink,\" und sinkt\",\"\").\""
 
 msgid "nr_insectwinter"
-msgstr "Es ist Winter, und Insekten können nur in Wüsten oder mit Hilfe des Nestwärme-Tranks Personen rekrutieren."
+msgstr "Insekten können wegen des Winterwetters in der kommenden Woche nur in Wüsten oder mit Hilfe des Nestwärme-Tranks Personen rekrutieren."
 
 msgid "spellfail_nomonsters"
 msgstr "\"$unit($unit) in $region($region): '$order($command)' - Dieser Zauber kann nicht auf Monster gezaubert werden.\""
@@ -2727,7 +2727,7 @@ msgid "volcanostartsmoke"
 msgstr "\"Aus dem Vulkankrater von $region($region) steigt plötzlich Rauch.\""
 
 msgid "nr_insectfall"
-msgstr "Es ist Spätherbst, und diese Woche ist die letzte vor dem Winter, in der Insekten rekrutieren können."
+msgstr "Es ist Spätherbst, und die kommende Woche ist die letzte vor dem Winter, in der Insekten rekrutieren können."
 
 msgid "error296"
 msgstr "\"$unit($unit) in $region($region): '$order($command)' - Hier werden niemals Bäume wachsen.\""
diff --git a/src/economy.c b/src/economy.c
index 3bb7b1066..ae5be2c77 100644
--- a/src/economy.c
+++ b/src/economy.c
@@ -477,7 +477,7 @@ static void recruit(unit * u, struct order *ord, econ_request ** recruitorders)
     if (rc == get_race(RC_INSECT)) {
         gamedate date;
         get_gamedate(turn, &date);
-        if (date.season == 0 && r->terrain != newterrain(T_DESERT)) {
+        if (date.season == SEASON_WINTER && r->terrain != newterrain(T_DESERT)) {
             bool usepotion = false;
             unit *u2;
 
diff --git a/src/reports.c b/src/reports.c
index ac413bfc6..9d9ab5f6f 100644
--- a/src/reports.c
+++ b/src/reports.c
@@ -1517,24 +1517,23 @@ static void cb_add_seen(region *r, unit *u, void *cbdata) {
     }
 }
 
-void report_warnings(faction *f, const gamedate *date)
+void report_warnings(faction *f, int now)
 {
     if (f->age < NewbieImmunity()) {
         ADDMSG(&f->msgs, msg_message("newbieimmunity", "turns",
             NewbieImmunity() - f->age));
     }
 
-    if (date) {
-        if (f->race == get_race(RC_INSECT)) {
-            if (date->season == 0) {
-                ADDMSG(&f->msgs, msg_message("nr_insectwinter", ""));
-            }
-            else {
-                gamedate next;
-                get_gamedate(date->turn + 1, &next);
-                if (next.season == 0) {
-                    ADDMSG(&f->msgs, msg_message("nr_insectfall", ""));
-                }
+    if (f->race == get_race(RC_INSECT)) {
+        gamedate date;
+        get_gamedate(now + 1, &date);
+
+        if (date.season == SEASON_WINTER) {
+            ADDMSG(&f->msgs, msg_message("nr_insectwinter", ""));
+        }
+        else if (date.season == SEASON_AUTUMN) {
+            if (get_gamedate(now + 2 + 2, &date)->season == SEASON_WINTER) {
+                ADDMSG(&f->msgs, msg_message("nr_insectfall", ""));
             }
         }
     }
@@ -1552,11 +1551,9 @@ void prepare_report(report_context *ctx, faction *f)
     static bool rule_region_owners;
     static bool rule_lighthouse_units;
     const struct building_type *bt_lighthouse = bt_find("lighthouse");
-    gamedate now;
 
     /* Insekten-Winter-Warnung */
-    get_gamedate(turn, &now);
-    report_warnings(f, &now);
+    report_warnings(f, turn);
 
     if (bt_lighthouse && config_changed(&config)) {
         rule_region_owners = config_token("rules.region_owner_pay_building", bt_lighthouse->_name);
diff --git a/src/reports.h b/src/reports.h
index 0bdaf7d8c..a6dddb1b4 100644
--- a/src/reports.h
+++ b/src/reports.h
@@ -115,7 +115,7 @@ extern "C" {
         int size, const struct faction *viewer, bool see_unit);
     int report_items(const struct unit *u, struct item *result, int size,
         const struct unit *owner, const struct faction *viewer);
-    void report_warnings(struct faction *f, const struct gamedate *date);
+    void report_warnings(struct faction *f, int now);
     void report_raceinfo(const struct race *rc, const struct locale *lang, char *buf, size_t length);
     void report_race_skills(const struct race *rc, char *zText, size_t length, const struct locale *lang);
     void report_item(const struct unit *owner, const struct item *i,
diff --git a/src/reports.test.c b/src/reports.test.c
index e86080b0e..703abc232 100644
--- a/src/reports.test.c
+++ b/src/reports.test.c
@@ -785,23 +785,48 @@ static void test_stealth_modifier(CuTest *tc) {
     test_teardown();
 }
 
+static void setup_calendar(void) {
+    months_per_year = 9;
+    month_season = malloc(sizeof(int) * months_per_year);
+    month_season[0] = SEASON_SUMMER;
+    month_season[1] = SEASON_AUTUMN;
+    month_season[2] = SEASON_AUTUMN;
+    month_season[3] = SEASON_WINTER;
+    month_season[4] = SEASON_WINTER;
+    month_season[5] = SEASON_WINTER;
+    month_season[6] = SEASON_SPRING;
+    month_season[7] = SEASON_SPRING;
+    month_season[8] = SEASON_SUMMER;
+}
+
 static void test_insect_warnings(CuTest *tc) {
     faction *f;
     gamedate gd;
 
-    /* OBS: in unit tests, get_gamedate always returns season = 0 */
     test_setup();
+    setup_calendar();
+    config_set_int("game.start", 184);
     test_inject_messagetypes();
     f = test_create_faction(test_create_race("insect"));
 
-    gd.turn = 0;
-    gd.season = 3;
-    report_warnings(f, &gd);
-    CuAssertPtrNotNull(tc, test_find_messagetype(f->msgs, "nr_insectfall"));
-
-    gd.season = 0;
-    report_warnings(f, &gd);
+    CuAssertIntEquals(tc, SEASON_AUTUMN, get_gamedate(1083, &gd)->season);
+    report_warnings(f, gd.turn);
+    CuAssertPtrEquals(tc, NULL, test_find_messagetype(f->msgs, "nr_insectfall"));
     CuAssertPtrNotNull(tc, test_find_messagetype(f->msgs, "nr_insectwinter"));
+    test_clear_messages(f);
+
+    CuAssertIntEquals(tc, SEASON_AUTUMN, get_gamedate(1082, &gd)->season);
+    report_warnings(f, gd.turn);
+    CuAssertPtrNotNull(tc, test_find_messagetype(f->msgs, "nr_insectfall"));
+    CuAssertPtrEquals(tc, NULL, test_find_messagetype(f->msgs, "nr_insectwinter"));
+    test_clear_messages(f);
+
+    CuAssertIntEquals(tc, SEASON_WINTER, get_gamedate(1084, &gd)->season);
+    report_warnings(f, gd.turn);
+    CuAssertPtrEquals(tc, NULL, test_find_messagetype(f->msgs, "nr_insectfall"));
+    CuAssertPtrNotNull(tc, test_find_messagetype(f->msgs, "nr_insectwinter"));
+    test_clear_messages(f);
+
     test_teardown();
 }
 
@@ -810,16 +835,16 @@ static void test_newbie_warning(CuTest *tc) {
 
     test_setup();
     test_inject_messagetypes();
-    f = test_create_faction(test_create_race("insect"));
+    f = test_create_faction(NULL);
     config_set_int("NewbieImmunity", 3);
 
     f->age = 2;
-    report_warnings(f, NULL);
+    report_warnings(f, 0);
     CuAssertPtrNotNull(tc, test_find_messagetype(f->msgs, "newbieimmunity"));
     test_clear_messages(f);
 
     f->age = 3;
-    report_warnings(f, NULL);
+    report_warnings(f, 0);
     CuAssertPtrEquals(tc, NULL, test_find_messagetype(f->msgs, "newbieimmunity"));
     test_clear_messages(f);
 

From 8d8857957095c8af259d6ddfac138022233f54d0 Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Thu, 2 Aug 2018 14:31:00 +0200
Subject: [PATCH 47/62] Refactor: extract can_recruit, write tests for insects.

---
 src/economy.c      | 152 +++++++++++++++++++++++----------------------
 src/economy.h      |   2 +
 src/economy.test.c |  30 +++++++++
 src/reports.test.c |  17 +----
 src/tests.c        |  15 +++++
 src/tests.h        |   1 +
 6 files changed, 128 insertions(+), 89 deletions(-)

diff --git a/src/economy.c b/src/economy.c
index ae5be2c77..dc7e6148b 100644
--- a/src/economy.c
+++ b/src/economy.c
@@ -426,6 +426,59 @@ static int recruit_cost(const faction * f, const race * rc)
     return -1;
 }
 
+message *can_recruit(unit *u, const race *rc, order *ord, int now)
+{
+    region *r = u->region;
+
+    /* this is a very special case because the recruiting unit may be empty
+    * at this point and we have to look at the creating unit instead. This
+    * is done in cansee, which is called indirectly by is_guarded(). */
+    if (is_guarded(r, u)) {
+        return msg_error(u, ord, 70);
+    }
+
+    if (rc == get_race(RC_INSECT)) {
+        gamedate date;
+        get_gamedate(now, &date);
+        if (date.season == SEASON_WINTER && r->terrain != newterrain(T_DESERT)) {
+            bool usepotion = false;
+            unit *u2;
+
+            for (u2 = r->units; u2; u2 = u2->next) {
+                if (fval(u2, UFL_WARMTH)) {
+                    usepotion = true;
+                    break;
+                }
+            }
+            if (!usepotion) {
+                return msg_error(u, ord, 98);
+            }
+        }
+        /* in Gletschern, Eisbergen gar nicht rekrutieren */
+        if (r_insectstalled(r)) {
+            return msg_error(u, ord, 97);
+        }
+    }
+    if (is_cursed(r->attribs, &ct_riotzone)) {
+        /* Die Region befindet sich in Aufruhr */
+        return msg_error(u, ord, 237);
+    }
+
+    if (rc && !playerrace(rc)) {
+        return msg_error(u, ord, 139);
+    }
+
+    if (fval(u, UFL_HERO)) {
+        return msg_feedback(u, ord, "error_herorecruit", "");
+    }
+    if (has_skill(u, SK_MAGIC)) {
+        /* error158;de;{unit} in {region}: '{command}' - Magier arbeiten
+        * grunds�tzlich nur alleine! */
+        return msg_error(u, ord, 158);
+    }
+    return NULL;
+}
+
 static void recruit(unit * u, struct order *ord, econ_request ** recruitorders)
 {
     region *r = u->region;
@@ -434,6 +487,7 @@ static void recruit(unit * u, struct order *ord, econ_request ** recruitorders)
     const faction *f = u->faction;
     const struct race *rc = u_race(u);
     int n;
+    message *msg;
 
     init_order_depr(ord);
     n = getint();
@@ -456,6 +510,7 @@ static void recruit(unit * u, struct order *ord, econ_request ** recruitorders)
             }
         }
     }
+
     if (recruitcost < 0) {
         rc = u_race(u);
         recruitcost = recruit_cost(f, rc);
@@ -463,95 +518,46 @@ static void recruit(unit * u, struct order *ord, econ_request ** recruitorders)
             recruitcost = INT_MAX;
         }
     }
-    assert(rc);
-    u_setrace(u, rc);
 
-    /* this is a very special case because the recruiting unit may be empty
-     * at this point and we have to look at the creating unit instead. This
-     * is done in cansee, which is called indirectly by is_guarded(). */
-    if (is_guarded(r, u)) {
-        cmistake(u, ord, 70, MSG_EVENT);
-        return;
-    }
-
-    if (rc == get_race(RC_INSECT)) {
-        gamedate date;
-        get_gamedate(turn, &date);
-        if (date.season == SEASON_WINTER && r->terrain != newterrain(T_DESERT)) {
-            bool usepotion = false;
-            unit *u2;
-
-            for (u2 = r->units; u2; u2 = u2->next)
-                if (fval(u2, UFL_WARMTH)) {
-                    usepotion = true;
-                    break;
-                }
-            if (!usepotion)
-            {
-                cmistake(u, ord, 98, MSG_EVENT);
-                return;
-            }
-        }
-        /* in Gletschern, Eisbergen gar nicht rekrutieren */
-        if (r_insectstalled(r)) {
-            cmistake(u, ord, 97, MSG_EVENT);
-            return;
-        }
-    }
-    if (is_cursed(r->attribs, &ct_riotzone)) {
-        /* Die Region befindet sich in Aufruhr */
-        cmistake(u, ord, 237, MSG_EVENT);
-        return;
-    }
-
-    if (recruitcost) {
+    if (recruitcost > 0) {
+        int pool;
         plane *pl = getplane(r);
-        if (pl && fval(pl, PFL_NORECRUITS)) {
+
+        if (pl && (pl->flags & PFL_NORECRUITS)) {
             ADDMSG(&u->faction->msgs, msg_feedback(u, ord, "error_pflnorecruit", ""));
             return;
         }
 
-        if (get_pooled(u, get_resourcetype(R_SILVER), GET_DEFAULT,
-            recruitcost) < recruitcost) {
+        pool = get_pooled(u, get_resourcetype(R_SILVER), GET_DEFAULT, recruitcost * n);
+        if (pool < recruitcost) {
             cmistake(u, ord, 142, MSG_EVENT);
             return;
         }
+        pool /= recruitcost;
+        if (n > pool) n = pool;
     }
-    if (!playerrace(rc)) {
-        cmistake(u, ord, 139, MSG_EVENT);
-        return;
-    }
-
-    if (fval(u, UFL_HERO)) {
-        ADDMSG(&u->faction->msgs, msg_feedback(u, ord, "error_herorecruit", ""));
-        return;
-    }
-    if (has_skill(u, SK_MAGIC)) {
-        /* error158;de;{unit} in {region}: '{command}' - Magier arbeiten
-         * grunds�tzlich nur alleine! */
-        cmistake(u, ord, 158, MSG_EVENT);
-        return;
-    }
-    if (has_skill(u, SK_ALCHEMY)
-        && count_skill(u->faction, SK_ALCHEMY) + n >
-        skill_limit(u->faction, SK_ALCHEMY)) {
-        cmistake(u, ord, 156, MSG_EVENT);
-        return;
-    }
-    if (recruitcost > 0) {
-        int pooled =
-            get_pooled(u, get_resourcetype(R_SILVER), GET_DEFAULT, recruitcost * n);
-        int pr = pooled / recruitcost;
-        if (n > pr) n = pr;
-    }
-
-    u->wants = n;
 
     if (!n) {
         cmistake(u, ord, 142, MSG_EVENT);
         return;
     }
+    if (has_skill(u, SK_ALCHEMY)) {
+        if (count_skill(u->faction, SK_ALCHEMY) + n > skill_limit(u->faction, SK_ALCHEMY)) {
+            cmistake(u, ord, 156, MSG_EVENT);
+            return;
+        }
+    }
 
+    assert(rc);
+    msg = can_recruit(u, rc, ord, turn);
+    if (msg) {
+        add_message(&u->faction->msgs, msg);
+        msg_release(msg);
+        return;
+    }
+
+    u_setrace(u, rc);
+    u->wants = n;
     o = (econ_request *)calloc(1, sizeof(econ_request));
     o->qty = n;
     o->unit = u;
diff --git a/src/economy.h b/src/economy.h
index 67762b513..9456a4c93 100644
--- a/src/economy.h
+++ b/src/economy.h
@@ -44,6 +44,7 @@ extern "C" {
 #define MAXNEWBIES								5
 
     struct unit;
+    struct race;
     struct region;
     struct faction;
     struct order;
@@ -94,6 +95,7 @@ extern "C" {
     void steal_cmd(struct unit * u, struct order *ord, struct econ_request ** stealorders);
     void expandstealing(struct region * r, struct econ_request * stealorders);
 
+    struct message *can_recruit(struct unit *u, const struct race *rc, struct order *ord, int now);
     void add_recruits(struct unit * u, int number, int wanted);
 
 #ifdef __cplusplus
diff --git a/src/economy.test.c b/src/economy.test.c
index 91d462770..90a4bd260 100644
--- a/src/economy.test.c
+++ b/src/economy.test.c
@@ -480,6 +480,35 @@ static void test_recruit(CuTest *tc) {
     test_teardown();
 }
 
+static void test_recruit_insect(CuTest *tc) {
+    unit *u;
+    faction *f;
+    message * msg;
+
+    test_setup();
+    test_create_calendar();
+    f = test_create_faction(test_create_race("insect"));
+    u = test_create_unit(f, test_create_region(0, 0, NULL));
+    u->thisorder = create_order(K_RECRUIT, f->locale, "%d", 1);
+
+    msg = can_recruit(u, f->race, u->thisorder, 1083); /* Autumn */
+    CuAssertPtrEquals(tc, NULL, msg);
+
+    msg = can_recruit(u, f->race, u->thisorder, 1084); /* Insects, Winter */
+    CuAssertPtrNotNull(tc, msg);
+    msg_release(msg);
+
+    u->flags |= UFL_WARMTH;
+    msg = can_recruit(u, f->race, u->thisorder, 1084); /* Insects, potion, Winter */
+    CuAssertPtrEquals(tc, NULL, msg);
+
+    u->flags = 0;
+    msg = can_recruit(u, NULL, u->thisorder, 1084); /* Other races, Winter */
+    CuAssertPtrEquals(tc, NULL, msg);
+
+    test_teardown();
+}
+
 static void test_income(CuTest *tc)
 {
     race *rc;
@@ -764,6 +793,7 @@ CuSuite *get_economy_suite(void)
     SUITE_ADD_TEST(suite, test_trade_insect);
     SUITE_ADD_TEST(suite, test_maintain_buildings);
     SUITE_ADD_TEST(suite, test_recruit);
+    SUITE_ADD_TEST(suite, test_recruit_insect);
     SUITE_ADD_TEST(suite, test_loot);
     SUITE_ADD_TEST(suite, test_expand_production);
     return suite;
diff --git a/src/reports.test.c b/src/reports.test.c
index 703abc232..65b5b06b0 100644
--- a/src/reports.test.c
+++ b/src/reports.test.c
@@ -785,27 +785,12 @@ static void test_stealth_modifier(CuTest *tc) {
     test_teardown();
 }
 
-static void setup_calendar(void) {
-    months_per_year = 9;
-    month_season = malloc(sizeof(int) * months_per_year);
-    month_season[0] = SEASON_SUMMER;
-    month_season[1] = SEASON_AUTUMN;
-    month_season[2] = SEASON_AUTUMN;
-    month_season[3] = SEASON_WINTER;
-    month_season[4] = SEASON_WINTER;
-    month_season[5] = SEASON_WINTER;
-    month_season[6] = SEASON_SPRING;
-    month_season[7] = SEASON_SPRING;
-    month_season[8] = SEASON_SUMMER;
-}
-
 static void test_insect_warnings(CuTest *tc) {
     faction *f;
     gamedate gd;
 
     test_setup();
-    setup_calendar();
-    config_set_int("game.start", 184);
+    test_create_calendar();
     test_inject_messagetypes();
     f = test_create_faction(test_create_race("insect"));
 
diff --git a/src/tests.c b/src/tests.c
index 09f533a9f..4ee38d195 100644
--- a/src/tests.c
+++ b/src/tests.c
@@ -260,6 +260,21 @@ static void test_reset(void) {
     }
 }
 
+void test_create_calendar(void) {
+    config_set_int("game.start", 184);
+    months_per_year = 9;
+    month_season = malloc(sizeof(int) * months_per_year);
+    month_season[0] = SEASON_SUMMER;
+    month_season[1] = SEASON_AUTUMN;
+    month_season[2] = SEASON_AUTUMN;
+    month_season[3] = SEASON_WINTER;
+    month_season[4] = SEASON_WINTER;
+    month_season[5] = SEASON_WINTER;
+    month_season[6] = SEASON_SPRING;
+    month_season[7] = SEASON_SPRING;
+    month_season[8] = SEASON_SUMMER;
+}
+
 void test_inject_messagetypes(void)
 {
     message_handle_missing(MESSAGE_MISSING_REPLACE);
diff --git a/src/tests.h b/src/tests.h
index dd8bed3c8..441db9e16 100644
--- a/src/tests.h
+++ b/src/tests.h
@@ -40,6 +40,7 @@ extern "C" {
     struct log_t * test_log_start(int flags, struct strlist **slist);
     void test_log_stop(struct log_t *log, struct strlist *slist);
 
+    void test_create_calendar(void);
     struct locale * test_create_locale(void);
     struct terrain_type * test_create_terrain(const char * name, int flags);
     struct race *test_create_race(const char *name);

From 116ff7247dcde0be4334748f47b555f7c11022df Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Thu, 2 Aug 2018 16:41:23 +0200
Subject: [PATCH 48/62] Remove unused "message" events (old NPC sh*t).

---
 src/laws.c | 11 -----------
 1 file changed, 11 deletions(-)

diff --git a/src/laws.c b/src/laws.c
index 35e4732f1..8d75b0897 100644
--- a/src/laws.c
+++ b/src/laws.c
@@ -2058,17 +2058,6 @@ int mail_cmd(unit * u, struct order *ord)
                 break;
             }
             else {
-                attrib *a = a_find(u2->attribs, &at_eventhandler);
-                if (a != NULL) {
-                    event_arg args[3];
-                    args[0].data.v = (void *)s;
-                    args[0].type = "string";
-                    args[1].data.v = (void *)u;
-                    args[1].type = "unit";
-                    args[2].type = NULL;
-                    handle_event(a, "message", args);
-                }
-
                 mailunit(r, u, n, ord, s);
             }
             return 0;

From fc0616ed1ec8d59636e74713ea0eb6f5e0051ddd Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Fri, 3 Aug 2018 06:47:29 +0200
Subject: [PATCH 49/62] kill lua triggers for npc logic, never used.

---
 scripts/tests/common.lua | 39 ---------------------------------------
 src/bind_unit.c          | 32 --------------------------------
 2 files changed, 71 deletions(-)

diff --git a/scripts/tests/common.lua b/scripts/tests/common.lua
index febcc2a3f..403db0a46 100644
--- a/scripts/tests/common.lua
+++ b/scripts/tests/common.lua
@@ -338,45 +338,6 @@ function test_message()
   return msg
 end
 
-function test_events()
-  local fail = 1
-  local function msg_handler(u, evt)
-    str = evt:get(0)
-    u2 = evt:get(1)
-    assert(u2~=nil)
-    assert(str=="Du Elf stinken")
-    message_unit(u, u2, "thanks unit, i got your message: " .. str)
-    message_faction(u, u2.faction, "thanks faction, i got your message: " .. str)
-    message_region(u, "thanks region, i got your message: " .. str)
-    fail = 0
-  end
-
-  plain = region.create(0, 0, "plain")
-  skill = 8
-
-  f = create_faction('elf')
-  f.age = 20
-
-  u = unit.create(f, plain)
-  u.number = 1
-  u:add_item("money", u.number*100)
-  u:clear_orders()
-  u:add_order("NUMMER PARTEI test")
-  u:add_handler("message", msg_handler)
-  msg = "BOTSCHAFT EINHEIT " .. itoa36(u.id) .. " Du~Elf~stinken"
-  f = create_faction('elf')
-  f.age = 20
-
-  u = unit.create(f, plain)
-  u.number = 1
-  u:add_item("money", u.number*100)
-  u:clear_orders()
-  u:add_order("NUMMER PARTEI eviL")
-  u:add_order(msg)
-  process_orders()
-  assert(fail==0)
-end
-
 function test_renumber_ship()
     local r = region.create(0, 0, "plain")
     local f = create_faction('human')
diff --git a/src/bind_unit.c b/src/bind_unit.c
index 7858aadf2..c6b045d33 100644
--- a/src/bind_unit.c
+++ b/src/bind_unit.c
@@ -468,35 +468,6 @@ static void fctr_done(trigger * t)
     free(fd);
 }
 
-static struct trigger_type tt_lua = {
-    "lua_event",
-    fctr_init,
-    fctr_done,
-    fctr_handle
-};
-
-static trigger *trigger_lua(struct unit *u, int handle)
-{
-    trigger *t = t_new(&tt_lua);
-    fctr_data *td = (fctr_data *)t->data.v;
-    td->target = u;
-    td->fhandle = handle;
-    return t;
-}
-
-static int tolua_unit_addhandler(lua_State * L)
-{
-    unit *self = (unit *)tolua_tousertype(L, 1, 0);
-    const char *ename = tolua_tostring(L, 2, 0);
-    int handle;
-
-    lua_pushvalue(L, 3);
-    handle = luaL_ref(L, LUA_REGISTRYINDEX);
-    add_trigger(&self->attribs, ename, trigger_lua(self, handle));
-
-    return 0;
-}
-
 static int tolua_unit_addnotice(lua_State * L)
 {
     unit *self = (unit *)tolua_tousertype(L, 1, 0);
@@ -1046,9 +1017,6 @@ void tolua_unit_open(lua_State * L)
 
             tolua_function(L, TOLUA_CAST "add_notice", tolua_unit_addnotice);
 
-            /*  npc logic: */
-            tolua_function(L, TOLUA_CAST "add_handler", tolua_unit_addhandler);
-
             tolua_variable(L, TOLUA_CAST "race_name", tolua_unit_get_racename,
                 tolua_unit_set_racename);
             tolua_function(L, TOLUA_CAST "add_spell", tolua_unit_addspell);

From 4978e95b9ae9c5b113617b2a1f29fe5f1f638bbc Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Fri, 3 Aug 2018 06:52:37 +0200
Subject: [PATCH 50/62] delete leftover npc callbacks.

---
 src/bind_unit.c | 13 -------------
 1 file changed, 13 deletions(-)

diff --git a/src/bind_unit.c b/src/bind_unit.c
index c6b045d33..fddcd5608 100644
--- a/src/bind_unit.c
+++ b/src/bind_unit.c
@@ -455,19 +455,6 @@ int fctr_handle(struct trigger *tp, void *data)
     return 0;
 }
 
-static void fctr_init(trigger * t)
-{
-    t->data.v = calloc(sizeof(fctr_data), 1);
-}
-
-static void fctr_done(trigger * t)
-{
-    fctr_data *fd = (fctr_data *)t->data.v;
-    lua_State *L = (lua_State *)global.vm_state;
-    luaL_unref(L, LUA_REGISTRYINDEX, fd->fhandle);
-    free(fd);
-}
-
 static int tolua_unit_addnotice(lua_State * L)
 {
     unit *self = (unit *)tolua_tousertype(L, 1, 0);

From ee8a02c42526d0614b84cb098ff913c2abab0ba0 Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Thu, 5 Jul 2018 20:06:32 +0200
Subject: [PATCH 51/62] autostudy framework

---
 src/CMakeLists.txt  |  1 +
 src/automate.c      | 71 +++++++++++++++++++++++++++++++++++++++++++++
 src/automate.h      | 28 ++++++++++++++++++
 src/kernel/config.c |  3 +-
 src/kernel/order.c  | 19 +++++++++---
 src/kernel/types.h  |  1 +
 src/kernel/unit.c   |  5 ++--
 src/kernel/unit.h   |  2 +-
 src/keyword.c       |  1 +
 src/keyword.h       |  1 +
 src/laws.c          |  2 ++
 src/study.c         |  8 ++---
 src/study.h         |  1 +
 13 files changed, 130 insertions(+), 13 deletions(-)
 create mode 100644 src/automate.c
 create mode 100644 src/automate.h

diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 5b267c0d5..0ff92bf2d 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -86,6 +86,7 @@ ENDIF()
 
 set (ERESSEA_SRC
   vortex.c
+  automate.c
   move.c
   piracy.c
   spells.c
diff --git a/src/automate.c b/src/automate.c
new file mode 100644
index 000000000..0855ff0e3
--- /dev/null
+++ b/src/automate.c
@@ -0,0 +1,71 @@
+#include <platform.h>
+
+#include "kernel/faction.h"
+#include "kernel/order.h"
+#include "kernel/region.h"
+#include "kernel/unit.h"
+
+#include "util/log.h"
+
+#include "automate.h"
+#include "keyword.h"
+#include "study.h"
+
+#include <stdlib.h>
+
+typedef struct student {
+    unit *u;
+    skill_t sk;
+    int level;
+} student;
+
+#define MAXSTUDENTS 128
+
+int cmp_students(const void *lhs, const void *rhs) {
+    const student *a = (const student *)lhs;
+    const student *b = (const student *)rhs;
+    if (a->sk == b->sk) {
+        /* sort by level, descending: */
+        return b->level - a->level;
+    }
+    /* order by skill */
+    return (int)a->sk - (int)b->sk;
+}
+
+void do_autostudy(region *r) {
+    unit *u;
+    int nstudents = 0;
+    student students[MAXSTUDENTS];
+
+    for (u = r->units; u; u = u->next) {
+        keyword_t kwd = getkeyword(u->thisorder);
+        if (kwd == K_AUTOSTUDY) {
+            student * st = students + nstudents;
+            if (++nstudents == MAXSTUDENTS) {
+                log_fatal("you must increase MAXSTUDENTS");
+            }
+            st->u = u;
+            init_order(u->thisorder, u->faction->locale);
+            st->sk = getskill(u->faction->locale);
+            st->level = effskill_study(u, st->sk);
+        }
+    }
+    if (nstudents > 0) {
+        int i, taught = 0;
+        skill_t sk = NOSKILL;
+        student *teacher = NULL, *student = NULL;
+
+        qsort(students, nstudents, sizeof(student), cmp_students);
+        for (i = 0; i != nstudents; ++i) {
+            if (students[i].u) {
+                if (sk == NOSKILL) {
+                    sk = students[i].sk;
+                }
+                else if (sk != students[i].sk) {
+                    continue;
+                }
+                students[i].u = NULL;
+            }
+        }
+    }
+}
diff --git a/src/automate.h b/src/automate.h
new file mode 100644
index 000000000..ded8f938b
--- /dev/null
+++ b/src/automate.h
@@ -0,0 +1,28 @@
+/*
+Copyright (c) 1998-2018, Enno Rehling <enno@eressea.de>
+Katja Zedel <katze@felidae.kn-bremen.de
+Christian Schlittchen <corwin@amber.kn-bremen.de>
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+**/
+
+#pragma once
+
+#ifndef H_GC_AUTOMATE
+#define H_GC_AUTOMATE
+
+struct region;
+
+void do_autostudy(struct region *r);
+
+#endif
diff --git a/src/kernel/config.c b/src/kernel/config.c
index ec45ddd0d..750d406f6 100644
--- a/src/kernel/config.c
+++ b/src/kernel/config.c
@@ -135,7 +135,8 @@ const char *parameters[MAXPARAMS] = {
     "GRUPPE",
     "PARTEITARNUNG",
     "BAEUME",
-    "ALLIANZ"
+    "ALLIANZ",
+    "AUTO"
 };
 
 int findoption(const char *s, const struct locale *lang)
diff --git a/src/kernel/order.c b/src/kernel/order.c
index e660edf81..ac2573758 100644
--- a/src/kernel/order.c
+++ b/src/kernel/order.c
@@ -214,12 +214,12 @@ static int create_data(keyword_t kwd, const char *s,
     order_data *data;
     int id;
 
-    assert(kwd!=NOKEYWORD);
+    assert(kwd != NOKEYWORD);
 
     if (!s || *s == 0) {
         return 0;
     }
-    if (kwd==K_STUDY) {
+    if (kwd == K_STUDY || kwd == K_AUTOSTUDY) {
         const char * sptr = s;
         skill_t sk = get_skill(parse_token_depr(&sptr), lang);
         if (sk != SK_MAGIC && sk != NOSKILL) {
@@ -332,6 +332,14 @@ order *parse_order(const char *s, const struct locale * lang)
                 sptr = sp;
             }
         }
+        else if (kwd == K_STUDY) {
+            const char *sp = sptr;
+            p = parse_token_depr(&sp);
+            if (p && isparam(p, lang, P_AUTO)) {
+                kwd = K_AUTOSTUDY;
+                sptr = sp;
+            }
+        }
         if (kwd != NOKEYWORD) {
             order *ord = (order *)malloc(sizeof(order));
             create_order_i(ord, kwd, sptr, persistent, noerror, lang);
@@ -366,6 +374,7 @@ bool is_repeated(keyword_t kwd)
     case K_STEAL:
     case K_SABOTAGE:
     case K_STUDY:
+    case K_AUTOSTUDY:
     case K_TEACH:
     case K_GROW:
     case K_PLANT:
@@ -406,6 +415,7 @@ bool is_exclusive(const order * ord)
     case K_STEAL:
     case K_SABOTAGE:
     case K_STUDY:
+    case K_AUTOSTUDY:
     case K_TEACH:
     case K_GROW:
     case K_PLANT:
@@ -447,6 +457,7 @@ bool is_long(keyword_t kwd)
     case K_STEAL:
     case K_SABOTAGE:
     case K_STUDY:
+    case K_AUTOSTUDY:
     case K_TEACH:
     case K_GROW:
     case K_PLANT:
@@ -541,7 +552,7 @@ keyword_t init_order(const struct order *ord, const struct locale *lang)
 
             assert(sk < MAXSKILLS);
             assert(lang);
-            assert(kwd == K_STUDY);
+            assert(kwd == K_STUDY || kwd == K_AUTOSTUDY);
             str = skillname(sk, lang);
             if (strchr(str, ' ') == NULL) {
                 init_tokens_str(str);
@@ -575,7 +586,7 @@ keyword_t init_order_depr(const struct order *ord)
 {
     if (ord) {
         keyword_t kwd = ORD_KEYWORD(ord);
-        assert(kwd != K_STUDY);
+        assert(kwd != K_STUDY && kwd != K_AUTOSTUDY);
     }
     return init_order(ord, NULL);
 }
diff --git a/src/kernel/types.h b/src/kernel/types.h
index 61f1268cc..48a4326fa 100644
--- a/src/kernel/types.h
+++ b/src/kernel/types.h
@@ -130,6 +130,7 @@ typedef enum {
   P_FACTIONSTEALTH,
   P_TREES,
   P_ALLIANCE,
+  P_AUTO,
   MAXPARAMS,
   NOPARAM 
 } param_t;
diff --git a/src/kernel/unit.c b/src/kernel/unit.c
index 07c6c0856..770723bea 100644
--- a/src/kernel/unit.c
+++ b/src/kernel/unit.c
@@ -1310,13 +1310,12 @@ int eff_skill(const unit * u, const skill *sv, const region *r)
     return 0;
 }
 
-int effskill_study(const unit * u, skill_t sk, const region * r)
+int effskill_study(const unit * u, skill_t sk)
 {
     skill *sv = unit_skill(u, sk);
     if (sv && sv->level > 0) {
         int mlevel = sv->level;
-        if (!r) r = u->region;
-        mlevel += get_modifier(u, sv->id, sv->level, r, true);
+        mlevel += get_modifier(u, sv->id, sv->level, u->region, true);
         if (mlevel > 0)
             return mlevel;
     }
diff --git a/src/kernel/unit.h b/src/kernel/unit.h
index ac61affde..a6135715c 100644
--- a/src/kernel/unit.h
+++ b/src/kernel/unit.h
@@ -163,7 +163,7 @@ extern "C" {
     void clone_men(const struct unit *src, struct unit *dst, int n); /* like transfer, but do not subtract from src */
 
     int eff_skill(const struct unit *u, const struct skill *sv, const struct region *r);
-    int effskill_study(const struct unit *u, skill_t sk, const struct region *r);
+    int effskill_study(const struct unit *u, skill_t sk);
 
     int get_modifier(const struct unit *u, skill_t sk, int level,
         const struct region *r, bool noitem);
diff --git a/src/keyword.c b/src/keyword.c
index 7519b6449..48726bcbc 100644
--- a/src/keyword.c
+++ b/src/keyword.c
@@ -149,5 +149,6 @@ const char *keywords[MAXKEYWORDS] = {
     "promote",
     "pay",
     "loot",
+    "autostudy",
 };
 
diff --git a/src/keyword.h b/src/keyword.h
index a9273c3f5..6e4832708 100644
--- a/src/keyword.h
+++ b/src/keyword.h
@@ -72,6 +72,7 @@ extern "C"
         K_PROMOTION,
         K_PAY,
         K_LOOT,
+        K_AUTOSTUDY,
         MAXKEYWORDS,
         NOKEYWORD
     } keyword_t;
diff --git a/src/laws.c b/src/laws.c
index 8d75b0897..80c730c8f 100644
--- a/src/laws.c
+++ b/src/laws.c
@@ -26,6 +26,7 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 #include <modules/gmcmd.h>
 
 #include "alchemy.h"
+#include "automate.h"
 #include "battle.h"
 #include "economy.h"
 #include "keyword.h"
@@ -4001,6 +4002,7 @@ void init_processor(void)
     }
 
     p += 10;
+    add_proc_region(p, do_autostudy, "study automation");
     add_proc_order(p, K_TEACH, teach_cmd, PROC_THISORDER | PROC_LONGORDER,
         "Lehren");
     p += 10;
diff --git a/src/study.c b/src/study.c
index 46da90b74..6df5b5eaf 100644
--- a/src/study.c
+++ b/src/study.c
@@ -69,7 +69,7 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 #define TEACH_ALL 1
 #define TEACH_FRIENDS
 
-static skill_t getskill(const struct locale *lang)
+skill_t getskill(const struct locale *lang)
 {
     char token[128];
     const char * s = gettoken(token, sizeof(token));
@@ -331,7 +331,7 @@ int teach_cmd(unit * teacher, struct order *ord)
                         sk = teachskill[t];
                     }
                     if (sk != NOSKILL
-                        && effskill_study(teacher, sk, 0) - TEACHDIFFERENCE > effskill_study(student, sk, 0)) {
+                        && effskill_study(teacher, sk) - TEACHDIFFERENCE > effskill_study(student, sk)) {
                         teaching -= teach_unit(teacher, student, teaching, sk, true, &academy_students);
                     }
                 }
@@ -343,7 +343,7 @@ int teach_cmd(unit * teacher, struct order *ord)
                     init_order(student->thisorder, student->faction->locale);
                     sk = getskill(student->faction->locale);
                     if (sk != NOSKILL
-                        && effskill_study(teacher, sk, 0) - TEACHDIFFERENCE >= effskill(student, sk, 0)) {
+                        && effskill_study(teacher, sk) - TEACHDIFFERENCE >= effskill(student, sk)) {
                         teaching -= teach_unit(teacher, student, teaching, sk, true, &academy_students);
                     }
                 }
@@ -432,7 +432,7 @@ int teach_cmd(unit * teacher, struct order *ord)
                 continue;
             }
 
-            if (effskill_study(student, sk, 0) > effskill_study(teacher, sk, 0)
+            if (effskill_study(student, sk, NULL) > effskill_study(teacher, sk)
                 - TEACHDIFFERENCE) {
                 if (feedback) {
                     ADDMSG(&teacher->faction->msgs, msg_feedback(teacher, ord, "teach_asgood",
diff --git a/src/study.h b/src/study.h
index b4b76bb8c..e99fb4808 100644
--- a/src/study.h
+++ b/src/study.h
@@ -33,6 +33,7 @@ extern "C" {
     int study_cmd(struct unit *u, struct order *ord);
 
     magic_t getmagicskill(const struct locale *lang);
+    skill_t getskill(const struct locale *lang);
     bool is_migrant(struct unit *u);
     int study_cost(struct unit *u, skill_t talent);
 

From f6b6904ced6fdf3460138ceef59a0f7db320d74e Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Sat, 7 Jul 2018 17:29:22 +0200
Subject: [PATCH 52/62] finish parser for K_AUTOSTUDY and P_AUTO.

---
 src/CMakeLists.txt      |  1 +
 src/automate.c          | 26 +++++++++++++-------------
 src/automate.h          | 12 ++++++++++++
 src/automate.test.c     | 38 ++++++++++++++++++++++++++++++++++++++
 src/kernel/order.c      |  2 +-
 src/kernel/order.test.c | 31 ++++++++++++++++++++++++++++---
 src/study.c             |  4 ++--
 src/test_eressea.c      |  3 ++-
 8 files changed, 97 insertions(+), 20 deletions(-)
 create mode 100644 src/automate.test.c

diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 0ff92bf2d..cf96cfbc2 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -188,6 +188,7 @@ set(TESTS_SRC
   tests.c
   academy.test.c
   alchemy.test.c
+  automate.test.c
   battle.test.c
   creport.test.c
   direction.test.c
diff --git a/src/automate.c b/src/automate.c
index 0855ff0e3..c1d4b384b 100644
--- a/src/automate.c
+++ b/src/automate.c
@@ -13,14 +13,6 @@
 
 #include <stdlib.h>
 
-typedef struct student {
-    unit *u;
-    skill_t sk;
-    int level;
-} student;
-
-#define MAXSTUDENTS 128
-
 int cmp_students(const void *lhs, const void *rhs) {
     const student *a = (const student *)lhs;
     const student *b = (const student *)rhs;
@@ -32,16 +24,16 @@ int cmp_students(const void *lhs, const void *rhs) {
     return (int)a->sk - (int)b->sk;
 }
 
-void do_autostudy(region *r) {
+int autostudy_init(student students[], int max_students, region *r)
+{
     unit *u;
     int nstudents = 0;
-    student students[MAXSTUDENTS];
 
     for (u = r->units; u; u = u->next) {
         keyword_t kwd = getkeyword(u->thisorder);
         if (kwd == K_AUTOSTUDY) {
             student * st = students + nstudents;
-            if (++nstudents == MAXSTUDENTS) {
+            if (++nstudents == max_students) {
                 log_fatal("you must increase MAXSTUDENTS");
             }
             st->u = u;
@@ -50,10 +42,18 @@ void do_autostudy(region *r) {
             st->level = effskill_study(u, st->sk);
         }
     }
+    return nstudents;
+}
+
+#define MAXSTUDENTS 128
+
+void do_autostudy(region *r) {
+    student students[MAXSTUDENTS];
+    int nstudents = autostudy_init(students, MAXSTUDENTS, r);
+
     if (nstudents > 0) {
-        int i, taught = 0;
+        int i;
         skill_t sk = NOSKILL;
-        student *teacher = NULL, *student = NULL;
 
         qsort(students, nstudents, sizeof(student), cmp_students);
         for (i = 0; i != nstudents; ++i) {
diff --git a/src/automate.h b/src/automate.h
index ded8f938b..49b052f56 100644
--- a/src/automate.h
+++ b/src/automate.h
@@ -21,8 +21,20 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 #ifndef H_GC_AUTOMATE
 #define H_GC_AUTOMATE
 
+#include "skill.h"
+
 struct region;
+struct unit;
+
+typedef struct student {
+    struct unit *u;
+    skill_t sk;
+    int level;
+    int learn;
+} student;
 
 void do_autostudy(struct region *r);
 
+int autostudy_init(student students[], int max_students, struct region *r);
+
 #endif
diff --git a/src/automate.test.c b/src/automate.test.c
new file mode 100644
index 000000000..d8dc8ed74
--- /dev/null
+++ b/src/automate.test.c
@@ -0,0 +1,38 @@
+#ifdef _MSC_VER
+#include <platform.h>
+#endif
+
+#include "automate.h"
+
+#include "kernel/faction.h"
+#include "kernel/order.h"
+#include "kernel/region.h"
+#include "kernel/unit.h"
+
+#include "tests.h"
+
+#include <CuTest.h>
+
+static void test_autostudy(CuTest *tc) {
+    student students[4];
+    unit *u1, *u2;
+    faction *f;
+    region *r;
+
+    test_setup();
+    r = test_create_plain(0, 0);
+    f = test_create_faction(NULL);
+    u1 = test_create_unit(f, r);
+    u1->thisorder = create_order(K_AUTOSTUDY, f->locale, skillnames[SK_ENTERTAINMENT]);
+    u2 = test_create_unit(f, r);
+    u2->thisorder = create_order(K_AUTOSTUDY, f->locale, skillnames[SK_ENTERTAINMENT]);
+    CuAssertIntEquals(tc, 2, autostudy_init(students, 4, r));
+    test_teardown();
+}
+
+CuSuite *get_automate_suite(void)
+{
+    CuSuite *suite = CuSuiteNew();
+    SUITE_ADD_TEST(suite, test_autostudy);
+    return suite;
+}
diff --git a/src/kernel/order.c b/src/kernel/order.c
index ac2573758..342f45577 100644
--- a/src/kernel/order.c
+++ b/src/kernel/order.c
@@ -84,7 +84,7 @@ char* get_command(const order *ord, const struct locale *lang, char *sbuffer, si
         sbs_strcat(&sbs, str);
         if (ord->id < 0) {
             skill_t sk = (skill_t)(100+ord->id);
-            assert(kwd == K_STUDY && sk != SK_MAGIC && sk < MAXSKILLS);
+            assert((kwd == K_STUDY || kwd == K_AUTOSTUDY) && sk != SK_MAGIC && sk < MAXSKILLS);
             str = skillname(sk, lang);
             if (str) {
                 if (strchr(str, ' ') == NULL) {
diff --git a/src/kernel/order.test.c b/src/kernel/order.test.c
index 3a2f44c56..d1f8b4176 100644
--- a/src/kernel/order.test.c
+++ b/src/kernel/order.test.c
@@ -113,6 +113,30 @@ static void test_parse_make(CuTest *tc) {
     test_teardown();
 }
 
+static void test_parse_autostudy(CuTest *tc) {
+    char cmd[32];
+    order *ord;
+    struct locale * lang;
+
+    test_setup();
+    lang = get_or_create_locale("en");
+    locale_setstring(lang, mkname("skill", skillnames[SK_ENTERTAINMENT]), "Entertainment");
+    locale_setstring(lang, keyword(K_STUDY), "STUDY");
+    locale_setstring(lang, keyword(K_AUTOSTUDY), "AUTOSTUDY");
+    locale_setstring(lang, parameters[P_AUTO], "AUTO");
+    init_locale(lang);
+
+    ord = parse_order("STUDY AUTO Entertainment", lang);
+    CuAssertPtrNotNull(tc, ord);
+    CuAssertIntEquals(tc, K_AUTOSTUDY, getkeyword(ord));
+    CuAssertStrEquals(tc, "AUTOSTUDY Entertainment", get_command(ord, lang, cmd, sizeof(cmd)));
+
+    CuAssertIntEquals(tc, K_AUTOSTUDY, init_order(ord, lang));
+    CuAssertStrEquals(tc, "Entertainment", getstrtoken());
+    free_order(ord);
+    test_teardown();
+}
+
 static void test_parse_make_temp(CuTest *tc) {
     char cmd[32];
     order *ord;
@@ -130,7 +154,7 @@ static void test_parse_make_temp(CuTest *tc) {
     CuAssertIntEquals(tc, K_MAKETEMP, getkeyword(ord));
     CuAssertStrEquals(tc, "MAKETEMP herp", get_command(ord, lang, cmd, sizeof(cmd)));
 
-    CuAssertIntEquals(tc, K_MAKETEMP, init_order_depr(ord));
+    CuAssertIntEquals(tc, K_MAKETEMP, init_order(ord, lang));
     CuAssertStrEquals(tc, "herp", getstrtoken());
     free_order(ord);
     test_teardown();
@@ -153,7 +177,7 @@ static void test_parse_maketemp(CuTest *tc) {
     CuAssertPtrNotNull(tc, ord);
     CuAssertStrEquals(tc, "MAKETEMP herp", get_command(ord, lang, cmd, sizeof(cmd)));
     CuAssertIntEquals(tc, K_MAKETEMP, getkeyword(ord));
-    CuAssertIntEquals(tc, K_MAKETEMP, init_order_depr(ord));
+    CuAssertIntEquals(tc, K_MAKETEMP, init_order(ord, lang));
     CuAssertStrEquals(tc, "herp", getstrtoken());
     free_order(ord);
     test_teardown();
@@ -167,7 +191,7 @@ static void test_init_order(CuTest *tc) {
 
     lang = get_or_create_locale("en");
     ord = create_order(K_MAKETEMP, lang, "hurr durr");
-    CuAssertIntEquals(tc, K_MAKETEMP, init_order_depr(ord));
+    CuAssertIntEquals(tc, K_MAKETEMP, init_order(ord, lang));
     CuAssertStrEquals(tc, "hurr", getstrtoken());
     CuAssertStrEquals(tc, "durr", getstrtoken());
     free_order(ord);
@@ -548,6 +572,7 @@ CuSuite *get_order_suite(void)
     SUITE_ADD_TEST(suite, test_study_order_quoted);
     SUITE_ADD_TEST(suite, test_parse_order);
     SUITE_ADD_TEST(suite, test_parse_make);
+    SUITE_ADD_TEST(suite, test_parse_autostudy);
     SUITE_ADD_TEST(suite, test_parse_make_temp);
     SUITE_ADD_TEST(suite, test_parse_maketemp);
     SUITE_ADD_TEST(suite, test_init_order);
diff --git a/src/study.c b/src/study.c
index 6df5b5eaf..217fe9505 100644
--- a/src/study.c
+++ b/src/study.c
@@ -343,7 +343,7 @@ int teach_cmd(unit * teacher, struct order *ord)
                     init_order(student->thisorder, student->faction->locale);
                     sk = getskill(student->faction->locale);
                     if (sk != NOSKILL
-                        && effskill_study(teacher, sk) - TEACHDIFFERENCE >= effskill(student, sk)) {
+                        && effskill_study(teacher, sk) - TEACHDIFFERENCE >= effskill(student, sk, NULL)) {
                         teaching -= teach_unit(teacher, student, teaching, sk, true, &academy_students);
                     }
                 }
@@ -432,7 +432,7 @@ int teach_cmd(unit * teacher, struct order *ord)
                 continue;
             }
 
-            if (effskill_study(student, sk, NULL) > effskill_study(teacher, sk)
+            if (effskill_study(student, sk) > effskill_study(teacher, sk)
                 - TEACHDIFFERENCE) {
                 if (feedback) {
                     ADDMSG(&teacher->faction->msgs, msg_feedback(teacher, ord, "teach_asgood",
diff --git a/src/test_eressea.c b/src/test_eressea.c
index c299fc09c..5684a8ad4 100644
--- a/src/test_eressea.c
+++ b/src/test_eressea.c
@@ -93,7 +93,6 @@ int RunAllTests(int argc, char *argv[])
     ADD_SUITE(xerewards);
     /* kernel */
     ADD_SUITE(academy);
-    ADD_SUITE(alchemy);
     ADD_SUITE(alliance);
     ADD_SUITE(ally);
     ADD_SUITE(building);
@@ -121,6 +120,8 @@ int RunAllTests(int argc, char *argv[])
     ADD_SUITE(spells);
     ADD_SUITE(unit);
     /* gamecode */
+    ADD_SUITE(alchemy);
+    ADD_SUITE(automate);
     ADD_SUITE(battle);
     ADD_SUITE(calendar);
     ADD_SUITE(creport);

From 0e754a31ac7e6ee19e801dd248445a274166f574 Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Sat, 7 Jul 2018 20:56:35 +0200
Subject: [PATCH 53/62] Test, sorting units by skill and level

---
 src/automate.c      |  5 ++++-
 src/automate.test.c | 22 ++++++++++++++++++++--
 2 files changed, 24 insertions(+), 3 deletions(-)

diff --git a/src/automate.c b/src/automate.c
index c1d4b384b..f195bb6db 100644
--- a/src/automate.c
+++ b/src/automate.c
@@ -40,8 +40,12 @@ int autostudy_init(student students[], int max_students, region *r)
             init_order(u->thisorder, u->faction->locale);
             st->sk = getskill(u->faction->locale);
             st->level = effskill_study(u, st->sk);
+            st->learn = 0;
         }
     }
+    if (nstudents > 0) {
+        qsort(students, nstudents, sizeof(student), cmp_students);
+    }
     return nstudents;
 }
 
@@ -55,7 +59,6 @@ void do_autostudy(region *r) {
         int i;
         skill_t sk = NOSKILL;
 
-        qsort(students, nstudents, sizeof(student), cmp_students);
         for (i = 0; i != nstudents; ++i) {
             if (students[i].u) {
                 if (sk == NOSKILL) {
diff --git a/src/automate.test.c b/src/automate.test.c
index d8dc8ed74..3ea640cdf 100644
--- a/src/automate.test.c
+++ b/src/automate.test.c
@@ -15,7 +15,7 @@
 
 static void test_autostudy(CuTest *tc) {
     student students[4];
-    unit *u1, *u2;
+    unit *u1, *u2, *u3;
     faction *f;
     region *r;
 
@@ -24,9 +24,27 @@ static void test_autostudy(CuTest *tc) {
     f = test_create_faction(NULL);
     u1 = test_create_unit(f, r);
     u1->thisorder = create_order(K_AUTOSTUDY, f->locale, skillnames[SK_ENTERTAINMENT]);
+    test_create_unit(f, r);
     u2 = test_create_unit(f, r);
     u2->thisorder = create_order(K_AUTOSTUDY, f->locale, skillnames[SK_ENTERTAINMENT]);
-    CuAssertIntEquals(tc, 2, autostudy_init(students, 4, r));
+    set_level(u2, SK_ENTERTAINMENT, 2);
+    u3 = test_create_unit(f, r);
+    u3->thisorder = create_order(K_AUTOSTUDY, f->locale, skillnames[SK_PERCEPTION]);
+    students[3].u = NULL;
+    CuAssertIntEquals(tc, 3, autostudy_init(students, 4, r));
+    CuAssertPtrEquals(tc, u2, students[0].u);
+    CuAssertIntEquals(tc, 2, students[0].level);
+    CuAssertIntEquals(tc, 0, students[0].learn);
+    CuAssertIntEquals(tc, SK_ENTERTAINMENT, students[0].sk);
+    CuAssertPtrEquals(tc, u1, students[1].u);
+    CuAssertIntEquals(tc, 0, students[1].level);
+    CuAssertIntEquals(tc, 0, students[1].learn);
+    CuAssertIntEquals(tc, SK_ENTERTAINMENT, students[1].sk);
+    CuAssertPtrEquals(tc, u3, students[2].u);
+    CuAssertIntEquals(tc, 0, students[2].level);
+    CuAssertIntEquals(tc, 0, students[2].learn);
+    CuAssertIntEquals(tc, SK_PERCEPTION, students[2].sk);
+    CuAssertPtrEquals(tc, NULL, students[3].u);
     test_teardown();
 }
 

From c8ebde3990b50537c8148f8dd02a2213efeb6ae0 Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Mon, 9 Jul 2018 03:31:13 +0200
Subject: [PATCH 54/62] autostudy continued (WIP)

---
 src/academy.c       |   8 ++--
 src/academy.h       |   2 +-
 src/automate.c      | 101 +++++++++++++++++++++++++++++++-------------
 src/automate.h      |   7 +--
 src/automate.test.c |  64 ++++++++++++++++++++--------
 src/study.c         |  86 ++++++++++++++++++-------------------
 6 files changed, 170 insertions(+), 98 deletions(-)

diff --git a/src/academy.c b/src/academy.c
index 5df13efa4..1298e9b57 100644
--- a/src/academy.c
+++ b/src/academy.c
@@ -33,13 +33,13 @@ void academy_teaching_bonus(struct unit *u, skill_t sk, int students) {
     }
 }
 
-bool academy_can_teach(unit *teacher, unit *student, skill_t sk) {
+bool academy_can_teach(unit *teacher, unit *scholar, skill_t sk) {
     const struct building_type *btype = bt_find("academy");
-    if (active_building(teacher, btype) && active_building(student, btype)) {
-        int j = study_cost(student, sk) * 2;
+    if (active_building(teacher, btype) && active_building(scholar, btype)) {
+        int j = study_cost(scholar, sk) * 2;
         if (j < 50) j = 50;
         /* kann Einheit das zahlen? */
-        return get_pooled(student, get_resourcetype(R_SILVER), GET_DEFAULT, j) >= j;
+        return get_pooled(scholar, get_resourcetype(R_SILVER), GET_DEFAULT, j) >= j;
         /* sonst nehmen sie nicht am Unterricht teil */
     }
     return false;
diff --git a/src/academy.h b/src/academy.h
index f6af93748..3c496d8fa 100644
--- a/src/academy.h
+++ b/src/academy.h
@@ -9,7 +9,7 @@ extern "C" {
 
     struct unit;
     void academy_teaching_bonus(struct unit *u, skill_t sk, int academy);
-    bool academy_can_teach(struct unit *teacher, struct unit *student, skill_t sk);
+    bool academy_can_teach(struct unit *teacher, struct unit *scholar, skill_t sk);
 #ifdef __cplusplus
 }
 #endif
diff --git a/src/automate.c b/src/automate.c
index f195bb6db..997f67bd8 100644
--- a/src/automate.c
+++ b/src/automate.c
@@ -13,9 +13,10 @@
 
 #include <stdlib.h>
 
-int cmp_students(const void *lhs, const void *rhs) {
-    const student *a = (const student *)lhs;
-    const student *b = (const student *)rhs;
+int cmp_scholars(const void *lhs, const void *rhs)
+{
+    const scholar *a = (const scholar *)lhs;
+    const scholar *b = (const scholar *)rhs;
     if (a->sk == b->sk) {
         /* sort by level, descending: */
         return b->level - a->level;
@@ -24,17 +25,17 @@ int cmp_students(const void *lhs, const void *rhs) {
     return (int)a->sk - (int)b->sk;
 }
 
-int autostudy_init(student students[], int max_students, region *r)
+int autostudy_init(scholar scholars[], int max_scholars, region *r)
 {
     unit *u;
-    int nstudents = 0;
+    int nscholars = 0;
 
     for (u = r->units; u; u = u->next) {
         keyword_t kwd = getkeyword(u->thisorder);
         if (kwd == K_AUTOSTUDY) {
-            student * st = students + nstudents;
-            if (++nstudents == max_students) {
-                log_fatal("you must increase MAXSTUDENTS");
+            scholar * st = scholars + nscholars;
+            if (++nscholars == max_scholars) {
+                log_fatal("you must increase MAXSCHOLARS");
             }
             st->u = u;
             init_order(u->thisorder, u->faction->locale);
@@ -43,31 +44,73 @@ int autostudy_init(student students[], int max_students, region *r)
             st->learn = 0;
         }
     }
-    if (nstudents > 0) {
-        qsort(students, nstudents, sizeof(student), cmp_students);
+    if (nscholars > 0) {
+        qsort(scholars, nscholars, sizeof(scholar), cmp_scholars);
     }
-    return nstudents;
+    return nscholars;
 }
 
-#define MAXSTUDENTS 128
-
-void do_autostudy(region *r) {
-    student students[MAXSTUDENTS];
-    int nstudents = autostudy_init(students, MAXSTUDENTS, r);
-
-    if (nstudents > 0) {
-        int i;
-        skill_t sk = NOSKILL;
-
-        for (i = 0; i != nstudents; ++i) {
-            if (students[i].u) {
-                if (sk == NOSKILL) {
-                    sk = students[i].sk;
+void autostudy_run(scholar scholars[], int nscholars)
+{
+    int i, t, s, ti = 0, si = 0, ts = 0, tt = 0;
+    skill_t sk = scholars[0].sk;
+    for (i = ti; i != nscholars && scholars[i].sk == sk; ++i) {
+        int mint;
+        ts += scholars[i].u->number; /* count total scholars */
+        mint = (ts + 10) / 11; /* need a minimum of ceil(ts/11) teachers */
+        while (mint>tt) {
+            tt += scholars[si++].u->number;
+        }
+    }
+    /* now si splits the teachers and students 1:10 */
+    /* first student must be 2 levels below first teacher: */
+    while (scholars[ti].level - TEACHDIFFERENCE > scholars[si].level) {
+        tt += scholars[si++].u->number;
+    }
+    /* invariant: unit ti can still teach i students */
+    i = scholars[ti].u->number * 10;
+    for (t = ti, s = si; t != si && s != nscholars; ++t) {
+        /* TODO: is there no constant for students per teacher? */
+        while (s != nscholars) {
+            int n = scholars[s].u->number;
+            scholars[s].learn += n;
+            if (i >= n) {
+                i -= n;
+                scholars[s].learn += n;
+                /* next student */
+            }
+            else {
+                scholars[s].learn += i;
+                /* go to next suitable teacher */
+                do {
+                    ++t;
                 }
-                else if (sk != students[i].sk) {
-                    continue;
-                }
-                students[i].u = NULL;
+                while (scholars[t].level - TEACHDIFFERENCE < scholars[s].level);
+            }
+        }
+    }
+}
+
+#define MAXSCHOLARS 128
+
+void do_autostudy(region *r)
+{
+    scholar scholars[MAXSCHOLARS];
+    int nscholars = autostudy_init(scholars, MAXSCHOLARS, r);
+
+    if (nscholars > 0) {
+        int i;
+        skill_t sk = NOSKILL;
+
+        for (i = 0; i != nscholars; ++i) {
+            if (scholars[i].u) {
+                if (sk == NOSKILL) {
+                    sk = scholars[i].sk;
+                }
+                else if (sk != scholars[i].sk) {
+                    continue;
+                }
+                scholars[i].u = NULL;
             }
         }
     }
diff --git a/src/automate.h b/src/automate.h
index 49b052f56..285fc6638 100644
--- a/src/automate.h
+++ b/src/automate.h
@@ -26,15 +26,16 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 struct region;
 struct unit;
 
-typedef struct student {
+typedef struct scholar {
     struct unit *u;
     skill_t sk;
     int level;
     int learn;
-} student;
+} scholar;
 
 void do_autostudy(struct region *r);
 
-int autostudy_init(student students[], int max_students, struct region *r);
+int autostudy_init(scholar scholars[], int max_scholars, struct region *r);
+void autostudy_run(scholar scholars[], int nscholars);
 
 #endif
diff --git a/src/automate.test.c b/src/automate.test.c
index 3ea640cdf..7404a2d9c 100644
--- a/src/automate.test.c
+++ b/src/automate.test.c
@@ -13,8 +13,8 @@
 
 #include <CuTest.h>
 
-static void test_autostudy(CuTest *tc) {
-    student students[4];
+static void test_autostudy_init(CuTest *tc) {
+    scholar scholars[4];
     unit *u1, *u2, *u3;
     faction *f;
     region *r;
@@ -30,27 +30,55 @@ static void test_autostudy(CuTest *tc) {
     set_level(u2, SK_ENTERTAINMENT, 2);
     u3 = test_create_unit(f, r);
     u3->thisorder = create_order(K_AUTOSTUDY, f->locale, skillnames[SK_PERCEPTION]);
-    students[3].u = NULL;
-    CuAssertIntEquals(tc, 3, autostudy_init(students, 4, r));
-    CuAssertPtrEquals(tc, u2, students[0].u);
-    CuAssertIntEquals(tc, 2, students[0].level);
-    CuAssertIntEquals(tc, 0, students[0].learn);
-    CuAssertIntEquals(tc, SK_ENTERTAINMENT, students[0].sk);
-    CuAssertPtrEquals(tc, u1, students[1].u);
-    CuAssertIntEquals(tc, 0, students[1].level);
-    CuAssertIntEquals(tc, 0, students[1].learn);
-    CuAssertIntEquals(tc, SK_ENTERTAINMENT, students[1].sk);
-    CuAssertPtrEquals(tc, u3, students[2].u);
-    CuAssertIntEquals(tc, 0, students[2].level);
-    CuAssertIntEquals(tc, 0, students[2].learn);
-    CuAssertIntEquals(tc, SK_PERCEPTION, students[2].sk);
-    CuAssertPtrEquals(tc, NULL, students[3].u);
+    scholars[3].u = NULL;
+    CuAssertIntEquals(tc, 3, autostudy_init(scholars, 4, r));
+    CuAssertPtrEquals(tc, u2, scholars[0].u);
+    CuAssertIntEquals(tc, 2, scholars[0].level);
+    CuAssertIntEquals(tc, 0, scholars[0].learn);
+    CuAssertIntEquals(tc, SK_ENTERTAINMENT, scholars[0].sk);
+    CuAssertPtrEquals(tc, u1, scholars[1].u);
+    CuAssertIntEquals(tc, 0, scholars[1].level);
+    CuAssertIntEquals(tc, 0, scholars[1].learn);
+    CuAssertIntEquals(tc, SK_ENTERTAINMENT, scholars[1].sk);
+    CuAssertPtrEquals(tc, u3, scholars[2].u);
+    CuAssertIntEquals(tc, 0, scholars[2].level);
+    CuAssertIntEquals(tc, 0, scholars[2].learn);
+    CuAssertIntEquals(tc, SK_PERCEPTION, scholars[2].sk);
+    CuAssertPtrEquals(tc, NULL, scholars[3].u);
     test_teardown();
 }
 
+static void test_autostudy_run(CuTest *tc) {
+    scholar scholars[4];
+    unit *u1, *u2, *u3;
+    faction *f;
+    region *r;
+
+    test_setup();
+    r = test_create_plain(0, 0);
+    f = test_create_faction(NULL);
+    u1 = test_create_unit(f, r);
+    u1->thisorder = create_order(K_AUTOSTUDY, f->locale, skillnames[SK_ENTERTAINMENT]);
+    set_number(u1, 2);
+    u2 = test_create_unit(f, r);
+    u2->thisorder = create_order(K_AUTOSTUDY, f->locale, skillnames[SK_ENTERTAINMENT]);
+    set_number(u2, 10);
+    set_level(u2, SK_ENTERTAINMENT, 2);
+    u3 = test_create_unit(f, r);
+    u3->thisorder = create_order(K_AUTOSTUDY, f->locale, skillnames[SK_PERCEPTION]);
+    set_number(u3, 20);
+    scholars[3].u = NULL;
+    CuAssertIntEquals(tc, 3, autostudy_init(scholars, 4, r));
+    autostudy_run(scholars, 3);
+    CuAssertIntEquals(tc, 0, scholars[0].learn);
+    CuAssertIntEquals(tc, 20, scholars[1].learn);
+    CuAssertIntEquals(tc, 30, scholars[2].learn);
+}
+
 CuSuite *get_automate_suite(void)
 {
     CuSuite *suite = CuSuiteNew();
-    SUITE_ADD_TEST(suite, test_autostudy);
+    SUITE_ADD_TEST(suite, test_autostudy_init);
+    SUITE_ADD_TEST(suite, test_autostudy_run);
     return suite;
 }
diff --git a/src/study.c b/src/study.c
index 217fe9505..0dc4386bc 100644
--- a/src/study.c
+++ b/src/study.c
@@ -195,37 +195,37 @@ const attrib_type at_learning = {
 
 #define EXPERIENCEDAYS 10
 
-static int study_days(unit * student, skill_t sk)
+static int study_days(unit * scholar, skill_t sk)
 {
     int speed = STUDYDAYS;
-    if (u_race(student)->study_speed) {
-        speed += u_race(student)->study_speed[sk];
+    if (u_race(scholar)->study_speed) {
+        speed += u_race(scholar)->study_speed[sk];
         if (speed < STUDYDAYS) {
-            skill *sv = unit_skill(student, sk);
+            skill *sv = unit_skill(scholar, sk);
             if (sv == 0) {
                 speed = STUDYDAYS;
             }
         }
     }
-    return student->number * speed;
+    return scholar->number * speed;
 }
 
 static int
-teach_unit(unit * teacher, unit * student, int nteaching, skill_t sk,
+teach_unit(unit * teacher, unit * scholar, int nteaching, skill_t sk,
     bool report, int *academy_students)
 {
     teaching_info *teach = NULL;
     attrib *a;
     int students;
 
-    if (magic_lowskill(student)) {
+    if (magic_lowskill(scholar)) {
         cmistake(teacher, teacher->thisorder, 292, MSG_EVENT);
         return 0;
     }
 
-    students = student->number;
+    students = scholar->number;
     /* subtract already taught students */
-    a = a_find(student->attribs, &at_learning);
+    a = a_find(scholar->attribs, &at_learning);
     if (a != NULL) {
         teach = (teaching_info *)a->data.v;
         students -= teach->students;
@@ -235,18 +235,18 @@ teach_unit(unit * teacher, unit * student, int nteaching, skill_t sk,
 
     if (students > 0) {
         if (teach == NULL) {
-            a = a_add(&student->attribs, a_new(&at_learning));
+            a = a_add(&scholar->attribs, a_new(&at_learning));
             teach = (teaching_info *)a->data.v;
         }
         selist_push(&teach->teachers, teacher);
         teach->days += students * STUDYDAYS;
         teach->students += students; 
 
-        if (student->building && teacher->building == student->building) {
+        if (scholar->building && teacher->building == scholar->building) {
             /* Solange Akademien groessenbeschraenkt sind, sollte Lehrer und
              * Student auch in unterschiedlichen Gebaeuden stehen duerfen */
             /* FIXME comment contradicts implementation */
-            if (academy_can_teach(teacher, student, sk)) {
+            if (academy_can_teach(teacher, scholar, sk)) {
                 /* Jeder Schueler zusaetzlich +10 Tage wenn in Uni. */
                 teach->days += students * EXPERIENCEDAYS;  /* learning erhoehen */
                 /* Lehrer zusaetzlich +1 Tag pro Schueler. */
@@ -304,7 +304,7 @@ int teach_cmd(unit * teacher, struct order *ord)
 #if TEACH_ALL
     if (getparam(teacher->faction->locale) == P_ANY) {
         skill_t sk;
-        unit *student;
+        unit *scholar;
         skill_t teachskill[MAXSKILLS];
         int t = 0;
 
@@ -313,15 +313,15 @@ int teach_cmd(unit * teacher, struct order *ord)
             teachskill[t] = getskill(teacher->faction->locale);
         } while (sk != NOSKILL);
 
-        for (student = r->units; teaching > 0 && student; student = student->next) {
-            if (LongHunger(student)) {
+        for (scholar = r->units; teaching > 0 && scholar; scholar = scholar->next) {
+            if (LongHunger(scholar)) {
                 continue;
             }
-            else if (student->faction == teacher->faction) {
-                if (getkeyword(student->thisorder) == K_STUDY) {
+            else if (scholar->faction == teacher->faction) {
+                if (getkeyword(scholar->thisorder) == K_STUDY) {
                     /* Input ist nun von student->thisorder !! */
-                    init_order(student->thisorder, student->faction->locale);
-                    sk = getskill(student->faction->locale);
+                    init_order(scholar->thisorder, scholar->faction->locale);
+                    sk = getskill(scholar->faction->locale);
                     if (sk != NOSKILL && teachskill[0] != NOSKILL) {
                         for (t = 0; teachskill[t] != NOSKILL; ++t) {
                             if (sk == teachskill[t]) {
@@ -331,20 +331,20 @@ int teach_cmd(unit * teacher, struct order *ord)
                         sk = teachskill[t];
                     }
                     if (sk != NOSKILL
-                        && effskill_study(teacher, sk) - TEACHDIFFERENCE > effskill_study(student, sk)) {
-                        teaching -= teach_unit(teacher, student, teaching, sk, true, &academy_students);
+                        && effskill_study(teacher, sk) - TEACHDIFFERENCE > effskill_study(scholar, sk)) {
+                        teaching -= teach_unit(teacher, scholar, teaching, sk, true, &academy_students);
                     }
                 }
             }
 #ifdef TEACH_FRIENDS
-            else if (alliedunit(teacher, student->faction, HELP_GUARD)) {
-                if (getkeyword(student->thisorder) == K_STUDY) {
+            else if (alliedunit(teacher, scholar->faction, HELP_GUARD)) {
+                if (getkeyword(scholar->thisorder) == K_STUDY) {
                     /* Input ist nun von student->thisorder !! */
-                    init_order(student->thisorder, student->faction->locale);
-                    sk = getskill(student->faction->locale);
+                    init_order(scholar->thisorder, scholar->faction->locale);
+                    sk = getskill(scholar->faction->locale);
                     if (sk != NOSKILL
-                        && effskill_study(teacher, sk) - TEACHDIFFERENCE >= effskill(student, sk, NULL)) {
-                        teaching -= teach_unit(teacher, student, teaching, sk, true, &academy_students);
+                        && effskill_study(teacher, sk) - TEACHDIFFERENCE >= effskill(scholar, sk, NULL)) {
+                        teaching -= teach_unit(teacher, scholar, teaching, sk, true, &academy_students);
                     }
                 }
             }
@@ -363,15 +363,15 @@ int teach_cmd(unit * teacher, struct order *ord)
 
         while (!parser_end()) {
             skill_t sk;
-            unit *student;
+            unit *scholar;
             bool feedback;
 
-            getunit(r, teacher->faction, &student);
+            getunit(r, teacher->faction, &scholar);
             ++count;
 
             /* Falls die Unit nicht gefunden wird, Fehler melden */
 
-            if (!student) {
+            if (!scholar) {
                 char tbuf[20];
                 const char *uid;
                 const char *token;
@@ -403,8 +403,8 @@ int teach_cmd(unit * teacher, struct order *ord)
                 continue;
             }
 
-            feedback = teacher->faction == student->faction
-                || alliedunit(student, teacher->faction, HELP_GUARD);
+            feedback = teacher->faction == scholar->faction
+                || alliedunit(scholar, teacher->faction, HELP_GUARD);
 
             /* Neuen Befehl zusammenbauen. TEMP-Einheiten werden automatisch in
              * ihre neuen Nummern uebersetzt. */
@@ -412,31 +412,31 @@ int teach_cmd(unit * teacher, struct order *ord)
                 strncat(zOrder, " ", sz - 1);
                 --sz;
             }
-            sz -= str_strlcpy(zOrder + 4096 - sz, itoa36(student->no), sz);
+            sz -= str_strlcpy(zOrder + 4096 - sz, itoa36(scholar->no), sz);
 
-            if (getkeyword(student->thisorder) != K_STUDY) {
+            if (getkeyword(scholar->thisorder) != K_STUDY) {
                 ADDMSG(&teacher->faction->msgs,
-                    msg_feedback(teacher, ord, "teach_nolearn", "student", student));
+                    msg_feedback(teacher, ord, "teach_nolearn", "student", scholar));
                 continue;
             }
 
             /* Input ist nun von student->thisorder !! */
             parser_pushstate();
-            init_order(student->thisorder, student->faction->locale);
-            sk = getskill(student->faction->locale);
+            init_order(scholar->thisorder, scholar->faction->locale);
+            sk = getskill(scholar->faction->locale);
             parser_popstate();
 
             if (sk == NOSKILL) {
                 ADDMSG(&teacher->faction->msgs,
-                    msg_feedback(teacher, ord, "teach_nolearn", "student", student));
+                    msg_feedback(teacher, ord, "teach_nolearn", "student", scholar));
                 continue;
             }
 
-            if (effskill_study(student, sk) > effskill_study(teacher, sk)
+            if (effskill_study(scholar, sk) > effskill_study(teacher, sk)
                 - TEACHDIFFERENCE) {
                 if (feedback) {
                     ADDMSG(&teacher->faction->msgs, msg_feedback(teacher, ord, "teach_asgood",
-                        "student", student));
+                        "student", scholar));
                 }
                 continue;
             }
@@ -444,18 +444,18 @@ int teach_cmd(unit * teacher, struct order *ord)
                 /* ist der Magier schon spezialisiert, so versteht er nur noch
                  * Lehrer seines Gebietes */
                 sc_mage *mage1 = get_mage_depr(teacher);
-                sc_mage *mage2 = get_mage_depr(student);
+                sc_mage *mage2 = get_mage_depr(scholar);
                 if (mage2 && mage1 && mage2->magietyp != M_GRAY
                     && mage1->magietyp != mage2->magietyp) {
                     if (feedback) {
                         ADDMSG(&teacher->faction->msgs, msg_feedback(teacher, ord,
-                            "error_different_magic", "target", student));
+                            "error_different_magic", "target", scholar));
                     }
                     continue;
                 }
             }
             sk_academy = sk;
-            teaching -= teach_unit(teacher, student, teaching, sk, false, &academy_students);
+            teaching -= teach_unit(teacher, scholar, teaching, sk, false, &academy_students);
         }
         new_order = create_order(K_TEACH, teacher->faction->locale, "%s", zOrder);
         replace_order(&teacher->orders, ord, new_order);

From b4cb1dfe8d48ed3a60586350017f15ac4280b2a6 Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Wed, 11 Jul 2018 21:03:00 +0200
Subject: [PATCH 55/62] finished autostudy

---
 src/automate.c      | 110 +++++++++++++++++++++++++++++++-------------
 src/automate.h      |   2 +
 src/automate.test.c |  34 ++++++++++++--
 3 files changed, 111 insertions(+), 35 deletions(-)

diff --git a/src/automate.c b/src/automate.c
index 997f67bd8..bf130acf3 100644
--- a/src/automate.c
+++ b/src/automate.c
@@ -12,6 +12,7 @@
 #include "study.h"
 
 #include <stdlib.h>
+#include <assert.h>
 
 int cmp_scholars(const void *lhs, const void *rhs)
 {
@@ -50,44 +51,89 @@ int autostudy_init(scholar scholars[], int max_scholars, region *r)
     return nscholars;
 }
 
+static void teaching(scholar *s, int n) {
+    assert(n <= s->u->number);
+    s->learn += n;
+}
+
+static void learning(scholar *s, int n) {
+    assert(n <= s->u->number);
+    s->learn += n;
+}
+
 void autostudy_run(scholar scholars[], int nscholars)
 {
-    int i, t, s, ti = 0, si = 0, ts = 0, tt = 0;
-    skill_t sk = scholars[0].sk;
-    for (i = ti; i != nscholars && scholars[i].sk == sk; ++i) {
-        int mint;
-        ts += scholars[i].u->number; /* count total scholars */
-        mint = (ts + 10) / 11; /* need a minimum of ceil(ts/11) teachers */
-        while (mint>tt) {
-            tt += scholars[si++].u->number;
-        }
-    }
-    /* now si splits the teachers and students 1:10 */
-    /* first student must be 2 levels below first teacher: */
-    while (scholars[ti].level - TEACHDIFFERENCE > scholars[si].level) {
-        tt += scholars[si++].u->number;
-    }
-    /* invariant: unit ti can still teach i students */
-    i = scholars[ti].u->number * 10;
-    for (t = ti, s = si; t != si && s != nscholars; ++t) {
-        /* TODO: is there no constant for students per teacher? */
-        while (s != nscholars) {
-            int n = scholars[s].u->number;
-            scholars[s].learn += n;
-            if (i >= n) {
-                i -= n;
-                scholars[s].learn += n;
-                /* next student */
+    int ti = 0;
+    while (ti != nscholars) {
+        skill_t sk = scholars[ti].sk;
+        int t, s, se, ts = 0, tt = 0, si = ti;
+        for (se = ti; se != nscholars && scholars[se].sk == sk; ++se) {
+            int mint;
+            ts += scholars[se].u->number; /* count total scholars */
+            mint = (ts + 10) / 11; /* need a minimum of ceil(ts/11) teachers */
+            for (; mint > tt && si != nscholars; ++si) {
+                tt += scholars[si].u->number;
             }
-            else {
-                scholars[s].learn += i;
-                /* go to next suitable teacher */
-                do {
-                    ++t;
+        }
+        /* now si splits the teachers and students 1:10 */
+        /* first student must be 2 levels below first teacher: */
+        for (; si != se && scholars[ti].level - TEACHDIFFERENCE > scholars[si].level; ++si) {
+            tt += scholars[si].u->number;
+        }
+        if (si == se) {
+            /* there are no students, so standard learning only */
+            for (t = ti; t != se; ++t) {
+                learning(scholars + t, scholars[t].u->number);
+            }
+        }
+        else {
+            /* invariant: unit ti can still teach i students */
+            int i = scholars[ti].u->number * STUDENTS_PER_TEACHER;
+            /* invariant: unit si has n students that can still be taught */
+            int n = scholars[si].u->number;
+            for (t = ti, s = si; t != si && s != se; ) {
+                if (i > n) {
+                    /* t has more than enough teaching capacity for s */
+                    i -= n;
+                    teaching(scholars + s, n);
+                    learning(scholars + s, scholars[s].u->number);
+                    /* next student, please: */
+                    if (++s == se) {
+                        continue;
+                    }
+                    n = scholars[s].u->number;
+                }
+                else {
+                    /* s gets partial credit and we need a new teacher */
+                    teaching(scholars + s, i);
+
+                    /* we are done with this teacher. any remaining people are regular learners: */
+                    if (scholars[t].u->number > 1) {
+                        /* remain = number - ceil(taught/10); */
+                        int remain = (STUDENTS_PER_TEACHER * scholars[t].u->number - i + STUDENTS_PER_TEACHER - 1) / STUDENTS_PER_TEACHER;
+                        learning(scholars + t, remain);
+                    }
+
+                    /* we want a new teacher for s. if any exists, it's next in the sequence. */
+                    if (++t == si) {
+                        continue;
+                    }
+                    if (scholars[t].level - TEACHDIFFERENCE < scholars[s].level) {
+                        /* next teacher cannot teach, we must skip students. */
+                        do {
+                            learning(scholars + s, (n - i));
+                            i = 0;
+                            if (++s == se) {
+                                continue;
+                            }
+                            n = scholars[s].u->number;
+                        } while (scholars[t].level - TEACHDIFFERENCE < scholars[s].level);
+                    }
+                    i = scholars[t].u->number * STUDENTS_PER_TEACHER;
                 }
-                while (scholars[t].level - TEACHDIFFERENCE < scholars[s].level);
             }
         }
+        ti = se;
     }
 }
 
diff --git a/src/automate.h b/src/automate.h
index 285fc6638..61fed6866 100644
--- a/src/automate.h
+++ b/src/automate.h
@@ -33,6 +33,8 @@ typedef struct scholar {
     int learn;
 } scholar;
 
+#define STUDENTS_PER_TEACHER 10
+
 void do_autostudy(struct region *r);
 
 int autostudy_init(scholar scholars[], int max_scholars, struct region *r);
diff --git a/src/automate.test.c b/src/automate.test.c
index 7404a2d9c..999cb25d4 100644
--- a/src/automate.test.c
+++ b/src/automate.test.c
@@ -60,19 +60,46 @@ static void test_autostudy_run(CuTest *tc) {
     u1 = test_create_unit(f, r);
     u1->thisorder = create_order(K_AUTOSTUDY, f->locale, skillnames[SK_ENTERTAINMENT]);
     set_number(u1, 2);
+    set_level(u1, SK_ENTERTAINMENT, 2);
     u2 = test_create_unit(f, r);
     u2->thisorder = create_order(K_AUTOSTUDY, f->locale, skillnames[SK_ENTERTAINMENT]);
     set_number(u2, 10);
-    set_level(u2, SK_ENTERTAINMENT, 2);
     u3 = test_create_unit(f, r);
     u3->thisorder = create_order(K_AUTOSTUDY, f->locale, skillnames[SK_PERCEPTION]);
-    set_number(u3, 20);
+    set_number(u3, 15);
     scholars[3].u = NULL;
     CuAssertIntEquals(tc, 3, autostudy_init(scholars, 4, r));
     autostudy_run(scholars, 3);
     CuAssertIntEquals(tc, 0, scholars[0].learn);
     CuAssertIntEquals(tc, 20, scholars[1].learn);
-    CuAssertIntEquals(tc, 30, scholars[2].learn);
+    CuAssertIntEquals(tc, 15, scholars[2].learn);
+}
+
+static void test_autostudy_run_noteachers(CuTest *tc) {
+    scholar scholars[4];
+    unit *u1, *u2, *u3;
+    faction *f;
+    region *r;
+
+    test_setup();
+    r = test_create_plain(0, 0);
+    f = test_create_faction(NULL);
+    u1 = test_create_unit(f, r);
+    u1->thisorder = create_order(K_AUTOSTUDY, f->locale, skillnames[SK_LUMBERJACK]);
+    set_number(u1, 2);
+    set_level(u1, SK_ENTERTAINMENT, 2);
+    u2 = test_create_unit(f, r);
+    u2->thisorder = create_order(K_AUTOSTUDY, f->locale, skillnames[SK_ENTERTAINMENT]);
+    set_number(u2, 10);
+    u3 = test_create_unit(f, r);
+    u3->thisorder = create_order(K_AUTOSTUDY, f->locale, skillnames[SK_PERCEPTION]);
+    set_number(u3, 15);
+    scholars[3].u = NULL;
+    CuAssertIntEquals(tc, 3, autostudy_init(scholars, 4, r));
+    autostudy_run(scholars, 3);
+    CuAssertIntEquals(tc, 2, scholars[0].learn);
+    CuAssertIntEquals(tc, 10, scholars[1].learn);
+    CuAssertIntEquals(tc, 15, scholars[2].learn);
 }
 
 CuSuite *get_automate_suite(void)
@@ -80,5 +107,6 @@ CuSuite *get_automate_suite(void)
     CuSuite *suite = CuSuiteNew();
     SUITE_ADD_TEST(suite, test_autostudy_init);
     SUITE_ADD_TEST(suite, test_autostudy_run);
+    SUITE_ADD_TEST(suite, test_autostudy_run_noteachers);
     return suite;
 }

From b0fb1e503224eb55968360c02b60bb3f63ae80cb Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Wed, 11 Jul 2018 21:07:31 +0200
Subject: [PATCH 56/62] actually do the learning.

---
 src/automate.c | 22 +++++-----------------
 src/study.c    |  6 +++---
 2 files changed, 8 insertions(+), 20 deletions(-)

diff --git a/src/automate.c b/src/automate.c
index bf130acf3..e08d7a625 100644
--- a/src/automate.c
+++ b/src/automate.c
@@ -142,22 +142,10 @@ void autostudy_run(scholar scholars[], int nscholars)
 void do_autostudy(region *r)
 {
     scholar scholars[MAXSCHOLARS];
-    int nscholars = autostudy_init(scholars, MAXSCHOLARS, r);
-
-    if (nscholars > 0) {
-        int i;
-        skill_t sk = NOSKILL;
-
-        for (i = 0; i != nscholars; ++i) {
-            if (scholars[i].u) {
-                if (sk == NOSKILL) {
-                    sk = scholars[i].sk;
-                }
-                else if (sk != scholars[i].sk) {
-                    continue;
-                }
-                scholars[i].u = NULL;
-            }
-        }
+    int i, nscholars = autostudy_init(scholars, MAXSCHOLARS, r);
+    autostudy_run(scholars, nscholars);
+    for (i = 0; i != nscholars; ++i) {
+        int days = STUDYDAYS * scholars[i].learn;
+        learn_skill(scholars[i].u, scholars[i].sk, days);
     }
 }
diff --git a/src/study.c b/src/study.c
index 0dc4386bc..bd29fc2a8 100644
--- a/src/study.c
+++ b/src/study.c
@@ -766,9 +766,6 @@ int study_cmd(unit * u, order * ord)
         days *= 2;
     }
 
-    if (fval(u, UFL_HUNGER))
-        days /= 2;
-
     learn_skill(u, sk, days);
     if (a != NULL) {
         if (teach->teachers) {
@@ -832,6 +829,9 @@ void learn_skill(unit *u, skill_t sk, int days) {
     int leveldays = STUDYDAYS * u->number;
     int weeks = 0;
 
+    if (fval(u, UFL_HUNGER)) {
+        days /= 2;
+    }
     assert(sk >= 0 && sk < MAXSKILLS);
     if (inject_learn_fun) {
         inject_learn_fun(u, sk, days);

From 2bb3e7601d96ae4703e53816332f6e99f199c9b1 Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno@eressea.de>
Date: Sun, 5 Aug 2018 06:42:31 +0200
Subject: [PATCH 57/62] merge conflict

---
 clibs          | 2 +-
 src/automate.c | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/clibs b/clibs
index d86c85254..66a891b38 160000
--- a/clibs
+++ b/clibs
@@ -1 +1 @@
-Subproject commit d86c8525489d7f11b7ba13e101bb59ecf160b871
+Subproject commit 66a891b383f1a51bb0d4e5cf002530f7f70bf7f4
diff --git a/src/automate.c b/src/automate.c
index e08d7a625..2f188f352 100644
--- a/src/automate.c
+++ b/src/automate.c
@@ -14,7 +14,7 @@
 #include <stdlib.h>
 #include <assert.h>
 
-int cmp_scholars(const void *lhs, const void *rhs)
+static int cmp_scholars(const void *lhs, const void *rhs)
 {
     const scholar *a = (const scholar *)lhs;
     const scholar *b = (const scholar *)rhs;

From 36b7104ce37b92aa6e455e7ee6ebda667590661e Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Sat, 14 Jul 2018 15:56:44 +0200
Subject: [PATCH 58/62] extract long_order_allowed for use in autostudy.

---
 src/automate.c      | 26 ++++++++++++++++++--------
 src/automate.test.c |  2 ++
 src/laws.c          | 31 ++++++++++++++++++++-----------
 src/laws.h          |  1 +
 src/laws.test.c     | 30 ++++++++++++++++++++++++++++++
 src/study.test.c    | 20 ++++++++++----------
 6 files changed, 81 insertions(+), 29 deletions(-)

diff --git a/src/automate.c b/src/automate.c
index 2f188f352..ec4ea83d8 100644
--- a/src/automate.c
+++ b/src/automate.c
@@ -1,6 +1,7 @@
 #include <platform.h>
 
 #include "kernel/faction.h"
+#include "kernel/messages.h"
 #include "kernel/order.h"
 #include "kernel/region.h"
 #include "kernel/unit.h"
@@ -9,6 +10,7 @@
 
 #include "automate.h"
 #include "keyword.h"
+#include "laws.h"
 #include "study.h"
 
 #include <stdlib.h>
@@ -34,15 +36,21 @@ int autostudy_init(scholar scholars[], int max_scholars, region *r)
     for (u = r->units; u; u = u->next) {
         keyword_t kwd = getkeyword(u->thisorder);
         if (kwd == K_AUTOSTUDY) {
-            scholar * st = scholars + nscholars;
-            if (++nscholars == max_scholars) {
-                log_fatal("you must increase MAXSCHOLARS");
+            if (long_order_allowed(u) && unit_can_study(u)) {
+                scholar * st = scholars + nscholars;
+                if (++nscholars == max_scholars) {
+                    log_fatal("you must increase MAXSCHOLARS");
+                }
+                st->u = u;
+                init_order(u->thisorder, u->faction->locale);
+                st->sk = getskill(u->faction->locale);
+                st->level = effskill_study(u, st->sk);
+                st->learn = 0;
+            }
+            else {
+                ADDMSG(&u->faction->msgs, msg_feedback(u, u->thisorder, "error_race_nolearn", "race",
+                    u_race(u)));
             }
-            st->u = u;
-            init_order(u->thisorder, u->faction->locale);
-            st->sk = getskill(u->faction->locale);
-            st->level = effskill_study(u, st->sk);
-            st->learn = 0;
         }
     }
     if (nscholars > 0) {
@@ -54,11 +62,13 @@ int autostudy_init(scholar scholars[], int max_scholars, region *r)
 static void teaching(scholar *s, int n) {
     assert(n <= s->u->number);
     s->learn += n;
+    fset(s->u, UFL_LONGACTION);
 }
 
 static void learning(scholar *s, int n) {
     assert(n <= s->u->number);
     s->learn += n;
+    fset(s->u, UFL_LONGACTION);
 }
 
 void autostudy_run(scholar scholars[], int nscholars)
diff --git a/src/automate.test.c b/src/automate.test.c
index 999cb25d4..a77376668 100644
--- a/src/automate.test.c
+++ b/src/automate.test.c
@@ -73,6 +73,7 @@ static void test_autostudy_run(CuTest *tc) {
     CuAssertIntEquals(tc, 0, scholars[0].learn);
     CuAssertIntEquals(tc, 20, scholars[1].learn);
     CuAssertIntEquals(tc, 15, scholars[2].learn);
+    test_teardown();
 }
 
 static void test_autostudy_run_noteachers(CuTest *tc) {
@@ -100,6 +101,7 @@ static void test_autostudy_run_noteachers(CuTest *tc) {
     CuAssertIntEquals(tc, 2, scholars[0].learn);
     CuAssertIntEquals(tc, 10, scholars[1].learn);
     CuAssertIntEquals(tc, 15, scholars[2].learn);
+    test_teardown();
 }
 
 CuSuite *get_automate_suite(void)
diff --git a/src/laws.c b/src/laws.c
index 80c730c8f..68f2894b3 100644
--- a/src/laws.c
+++ b/src/laws.c
@@ -3637,6 +3637,24 @@ void add_proc_unit(int priority, void(*process) (unit *), const char *name)
     }
 }
 
+bool long_order_allowed(const unit *u)
+{
+    const region *r = u->region;
+    if (fval(u, UFL_LONGACTION)) {
+        /* this message was already given in laws.update_long_order
+        cmistake(u, ord, 52, MSG_PRODUCE);
+        */
+        return false;
+    }
+    else if (fval(r->terrain, SEA_REGION)
+        && u_race(u) != get_race(RC_AQUARIAN)
+        && !(u_race(u)->flags & RCF_SWIM)) {
+        /* error message disabled by popular demand */
+        return false;
+    }
+    return true;
+}
+
 /* per priority, execute processors in order from PR_GLOBAL down to PR_ORDER */
 void process(void)
 {
@@ -3706,16 +3724,7 @@ void process(void)
                                         cmistake(u, ord, 224, MSG_MAGIC);
                                         ord = NULL;
                                     }
-                                    else if (fval(u, UFL_LONGACTION)) {
-                                        /* this message was already given in laws.update_long_order
-                                           cmistake(u, ord, 52, MSG_PRODUCE);
-                                           */
-                                        ord = NULL;
-                                    }
-                                    else if (fval(r->terrain, SEA_REGION)
-                                        && u_race(u) != get_race(RC_AQUARIAN)
-                                        && !(u_race(u)->flags & RCF_SWIM)) {
-                                        /* error message disabled by popular demand */
+                                    else if (!long_order_allowed(u)) {
                                         ord = NULL;
                                     }
                                 }
@@ -4157,7 +4166,7 @@ void update_subscriptions(void)
 /** determine if unit can be seen by faction
  * @param f -- the observiong faction
  * @param u -- the unit that is observed
- * @param r -- the region that u is obesrved in (see below)
+ * @param r -- the region that u is obesrved from (see below)
  * @param m -- terrain modifier to stealth
  * 
  * r kann != u->region sein, wenn es um Durchreisen geht,
diff --git a/src/laws.h b/src/laws.h
index ae2c712ca..0b5db17c7 100755
--- a/src/laws.h
+++ b/src/laws.h
@@ -66,6 +66,7 @@ extern "C" {
     void update_long_order(struct unit *u);
     void sinkships(struct region * r);
     void do_enter(struct region *r, bool is_final_attempt);
+    bool long_order_allowed(const struct unit *u);
 
     int password_cmd(struct unit *u, struct order *ord);
     int banner_cmd(struct unit *u, struct order *ord);
diff --git a/src/laws.test.c b/src/laws.test.c
index 8d13b7c81..c7c7a6682 100644
--- a/src/laws.test.c
+++ b/src/laws.test.c
@@ -1759,6 +1759,34 @@ static void test_nmr_timeout(CuTest *tc) {
     test_teardown();
 }
 
+static void test_long_orders(CuTest *tc) {
+    unit *u;
+
+    test_setup();
+    u = test_create_unit(test_create_faction(NULL), test_create_plain(0, 0));
+    CuAssertTrue(tc, long_order_allowed(u));
+    u->flags |= UFL_LONGACTION;
+    CuAssertTrue(tc, !long_order_allowed(u));
+    test_teardown();
+}
+
+static void test_long_order_on_ocean(CuTest *tc) {
+    unit *u;
+    race * rc;
+
+    test_setup();
+    rc = test_create_race("pikachu");
+    u = test_create_unit(test_create_faction(rc), test_create_ocean(0, 0));
+    CuAssertTrue(tc, !long_order_allowed(u));
+    rc->flags |= RCF_SWIM;
+    CuAssertTrue(tc, long_order_allowed(u));
+
+    rc = test_create_race("aquarian");
+    u = test_create_unit(test_create_faction(rc), u->region);
+    CuAssertTrue(tc, long_order_allowed(u));
+    test_teardown();
+}
+
 CuSuite *get_laws_suite(void)
 {
     CuSuite *suite = CuSuiteNew();
@@ -1831,6 +1859,8 @@ CuSuite *get_laws_suite(void)
     SUITE_ADD_TEST(suite, test_cansee_ring);
     SUITE_ADD_TEST(suite, test_cansee_sphere);
     SUITE_ADD_TEST(suite, test_nmr_timeout);
+    SUITE_ADD_TEST(suite, test_long_orders);
+    SUITE_ADD_TEST(suite, test_long_order_on_ocean);
 
     return suite;
 }
diff --git a/src/study.test.c b/src/study.test.c
index c1d0b0841..a63be7cf1 100644
--- a/src/study.test.c
+++ b/src/study.test.c
@@ -89,15 +89,15 @@ static void setup_teacher(study_fixture *fix, skill_t sk) {
     setup_locale(lang);
     fix->u = test_create_unit(f, r);
     assert(fix->u);
-    fix->u->thisorder = create_order(K_STUDY, f->locale, "%s", skillnames[sk]);
+    fix->u->thisorder = create_order(K_STUDY, f->locale, skillnames[sk]);
 
     fix->teachers[0] = test_create_unit(f, r);
     assert(fix->teachers[0]);
-    fix->teachers[0]->thisorder = create_order(K_TEACH, f->locale, "%s", itoa36(fix->u->no));
+    fix->teachers[0]->thisorder = create_order(K_TEACH, f->locale, itoa36(fix->u->no));
 
     fix->teachers[1] = test_create_unit(f, r);
     assert(fix->teachers[1]);
-    fix->teachers[1]->thisorder = create_order(K_TEACH, f->locale, "%s", itoa36(fix->u->no));
+    fix->teachers[1]->thisorder = create_order(K_TEACH, f->locale, itoa36(fix->u->no));
     test_clear_messages(f);
 }
 
@@ -110,7 +110,7 @@ static void test_study_no_teacher(CuTest *tc) {
     CuAssertPtrNotNull(tc, sv = unit_skill(fix.u, SK_CROSSBOW));
     CuAssertIntEquals(tc, 1, sv->level);
     CuAssertIntEquals(tc, 2, sv->weeks);
-    CuAssertPtrEquals(tc, 0, test_get_last_message(fix.u->faction->msgs));
+    CuAssertPtrEquals(tc, NULL, test_get_last_message(fix.u->faction->msgs));
     test_teardown();
 }
 
@@ -121,7 +121,7 @@ static void test_study_with_teacher(CuTest *tc) {
     setup_teacher(&fix, SK_CROSSBOW);
     set_level(fix.teachers[0], SK_CROSSBOW, TEACHDIFFERENCE);
     teach_cmd(fix.teachers[0], fix.teachers[0]->thisorder);
-    CuAssertPtrEquals(tc, 0, test_get_last_message(fix.u->faction->msgs));
+    CuAssertPtrEquals(tc, NULL, test_get_last_message(fix.u->faction->msgs));
     study_cmd(fix.u, fix.u->thisorder);
     CuAssertPtrNotNull(tc, sv = unit_skill(fix.u, SK_CROSSBOW));
     CuAssertIntEquals(tc, 1, sv->level);
@@ -288,7 +288,7 @@ static void test_academy_bonus(CuTest *tc) {
     u1 = test_create_unit(u->faction, u->region);
     u3 = test_create_unit(u->faction, u->region);
     u0->thisorder = create_order(K_TEACH, loc, "%s %s", itoa36(u3->no), itoa36(u1->no));
-    u->thisorder = create_order(K_TEACH, loc, "%s", itoa36(u1->no));
+    u->thisorder = create_order(K_TEACH, loc, itoa36(u1->no));
     u1->thisorder = create_order(K_STUDY, loc, skillnames[SK_CROSSBOW]);
     u3->thisorder = create_order(K_STUDY, loc, skillnames[SK_CROSSBOW]);
     
@@ -405,7 +405,7 @@ static void test_study_magic(CuTest *tc) {
     f = test_create_faction(NULL);
     lang = f->locale;
     u = test_create_unit(f, test_create_region(0, 0, NULL));
-    u->thisorder = create_order(K_STUDY, lang, "%s", skillnames[SK_MAGIC]);
+    u->thisorder = create_order(K_STUDY, lang, skillnames[SK_MAGIC]);
     itype = test_create_silver();
 
     CuAssertIntEquals(tc, -1, study_cmd(u, u->thisorder));
@@ -423,7 +423,7 @@ static void test_study_magic(CuTest *tc) {
     CuAssertIntEquals(tc, M_GWYRRD, f->magiegebiet);
     CuAssertIntEquals(tc, 0, i_get(u->items, itype));
     CuAssertPtrNotNull(tc, get_mage_depr(u));
-    CuAssertPtrEquals(tc, 0, test_find_messagetype(f->msgs, "error65"));
+    CuAssertPtrEquals(tc, NULL, test_find_messagetype(f->msgs, "error65"));
     CuAssertIntEquals(tc, M_GWYRRD, get_mage_depr(u)->magietyp);
 
     test_teardown();
@@ -491,12 +491,12 @@ static void test_teach_magic(CuTest *tc) {
     f = test_create_faction(NULL);
     f->magiegebiet = M_GWYRRD;
     u = test_create_unit(f, test_create_region(0, 0, NULL));
-    u->thisorder = create_order(K_STUDY, f->locale, "%s", skillnames[SK_MAGIC]);
+    u->thisorder = create_order(K_STUDY, f->locale, skillnames[SK_MAGIC]);
     i_change(&u->items, itype, study_cost(u, SK_MAGIC));
     ut = test_create_unit(f, u->region);
     set_level(ut, SK_MAGIC, TEACHDIFFERENCE);
     create_mage(ut, M_GWYRRD);
-    ut->thisorder = create_order(K_TEACH, f->locale, "%s", itoa36(u->no));
+    ut->thisorder = create_order(K_TEACH, f->locale, itoa36(u->no));
     learn_inject();
     teach_cmd(ut, ut->thisorder);
     study_cmd(u, u->thisorder);

From 22d0fe5693bd943aa5d8c911dba328f3a8fb75a1 Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno@eressea.de>
Date: Mon, 16 Jul 2018 10:53:12 +0200
Subject: [PATCH 59/62] missing include, do not use fset

---
 src/.DS_Store      | Bin 0 -> 8196 bytes
 src/automate.c     |   4 ++--
 src/util/.DS_Store | Bin 0 -> 6148 bytes
 3 files changed, 2 insertions(+), 2 deletions(-)
 create mode 100644 src/.DS_Store
 create mode 100644 src/util/.DS_Store

diff --git a/src/.DS_Store b/src/.DS_Store
new file mode 100644
index 0000000000000000000000000000000000000000..f331cdebb3c73099a3bcd9d817441024e2973339
GIT binary patch
literal 8196
zcmeHMTWl0n82-QQg&8{1=~`i1ob1-67O{kuAOumny>V+v8`{z>8p`g@2&2=TvNO9y
zBxW_yXbe%`Pz?Iui>a3v1$|UsjQU_?UudEx>We<83BHgR<9}vmNlWR2i7`axB<Fl{
z{{KIh`Q|%w_RJCy`igotQH+Qr&H~j2E^d&7U*u&e5?E>&!R^U%O8r^O&Jsl`%c4U=
z$Uw+I$Uw+I$Uw-z&A<Tf*&<0R?EAhqEJFrD25w0P#QPz^Szs!_vX9=YgA4Bnz*x&>
zFY&c1E*KL5rUESc$R1Acn=<-M5gak#H)neyh)V@n_R()n2o4_zMn-T#fj2t$pYXy7
zDIdc!WFTZ<IRoPCUQ0!C$s}v>`8_*pnU34lwSY)XZC(8;NnYKc+@|dx8a|+mOsmn7
zY0Qnxx~83X4jBc9aawJ3j@xtla&~^SU<m(Y&bEp(d1qQ}%4Muv(aN|+uVsyyrwq0=
zI=O;vS);aNif*RTu5r>8zGbfTVaG1G{a$C&s#<i6g9i6ymW%=J(Mg*;q-dx@o?o*r
zzG36$tven_4OSY}rl``ateq^Gj+wQLi8)huk56TsoRQbf{ISEPZsc9_QPU{MP3V-z
z^O+f=5{<RAuD?^$u8*$vk}hV=6GhW~LQP6iC8{<xCfYRR5uK-Uz}7`?y859cd!F_-
zO?y~)`U?h^IydkpwMpxg)r>Um@T`5y7NuKO9eMn5Gp~CVZJV-PQgwO4%47}8H|<n*
zN$QY1S@Ie8jm=7rB&T!s%naX*;|<%}r)fvTu)`V09ho)qJf+Wh?ABOIRBO}rO45|@
z_UbxMDVaJo>j@*7+UN@;S=EV0h@4Dy1qO*U+DZd7Oh>6ePtqAWOY`&rU7(Bf8GS_+
zTA-ikH~O8f&>wUa5lD!l6-l(?Zgijv526Pt^kP2_VG?O%kcAEdkHLY966SCk&*C{e
zj~DO~&f-;^!|Qkp@8UhYkB{&PzQ#AWgbKdJclaJZ;}=}PpOL!A>PYZAHWF+RG+Mn4
zA0y3-i?u!*e5*QlW39(`@7cRg8M=A3etC`1((TQ6tX-FA-?U}h&WHN0$>6%DzgUBw
z=zLXqYOAWrd)P~4T=7?e5}#mE8Yfp;5b7$ic%;humeyGOuJ{ItwWbEuj`%$iuLd>Z
z^>uG&Tv2OS__9~+>Q>@v9cx_js`qVI6t$KYf$zP47klej?!LE2V=u8!45M_!8)WI<
zlI|CDiGHA8S+rN_FVv#}F|@H<H)99OwHE^z#BPjW6bCVe2^_(6Alw<)IDrC-_cWeD
z8BgOGyofU_-IwtSUSsK=#~XMPZ(|<su!t|<B8&KAe2Onw!k4iSNV6*0;iZzrkBcQ+
z8Y|>2JAaIA)%d01VJ&3fA21;Heois*-W*){`@a_y79j&61OHD3P&+U-(8u?-(yk)T
z+6m4_Ig2FX%07BcxLEQbfa}-(Fy!k5hl)0-0LwnIh0}lkAz=AW*Kq&$9z|H(LJa&3
DfC<08

literal 0
HcmV?d00001

diff --git a/src/automate.c b/src/automate.c
index ec4ea83d8..a74cf770b 100644
--- a/src/automate.c
+++ b/src/automate.c
@@ -62,13 +62,13 @@ int autostudy_init(scholar scholars[], int max_scholars, region *r)
 static void teaching(scholar *s, int n) {
     assert(n <= s->u->number);
     s->learn += n;
-    fset(s->u, UFL_LONGACTION);
+    s->u->flags |= UFL_LONGACTION;
 }
 
 static void learning(scholar *s, int n) {
     assert(n <= s->u->number);
     s->learn += n;
-    fset(s->u, UFL_LONGACTION);
+    s->u->flags |= UFL_LONGACTION;
 }
 
 void autostudy_run(scholar scholars[], int nscholars)
diff --git a/src/util/.DS_Store b/src/util/.DS_Store
new file mode 100644
index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6
GIT binary patch
literal 6148
zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3
zem<@ulZcFPQ@L2!n>{z**<q8>++&mCkOWA81W14cNZ<zv;LbK1Poaz?KmsK2CSc!(
z0ynLxE!0092;Krf2c+FF_Fe*7ECH>lEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ
zLs35+`xjp>T0<F0fCPF1$Cyrb|F7^5{eNG?83~ZUUlGt@xh*qZDeu<Z%US-OSsOPv
j)R!Z4KLME7ReXlK;d!wEw5GODWMKRea10D2@KpjYNUI8I

literal 0
HcmV?d00001


From af445e5ca1ac53fe00403d0b998fcc77143fc74e Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Tue, 21 Aug 2018 22:25:04 +0200
Subject: [PATCH 60/62] =?UTF-8?q?BUG=202477=20Null-Personen=20Meldungen=20?=
 =?UTF-8?q?sind=20unn=C3=B6tig.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/monsters.c | 67 ++++++++++++++++++++++++++++----------------------
 1 file changed, 37 insertions(+), 30 deletions(-)

diff --git a/src/monsters.c b/src/monsters.c
index bbeb483b8..a2d2b2121 100644
--- a/src/monsters.c
+++ b/src/monsters.c
@@ -1016,37 +1016,42 @@ static void eaten_by_monster(unit * u)
 {
     /* adjustment for smaller worlds */
     double multi = RESOURCE_QUANTITY * newterrain(T_PLAIN)->size / 10000.0;
-    int n = 0;
-    int horse = -1;
     const resource_type *rhorse = get_resourcetype(R_HORSE);
     const race *rc = u_race(u);
-    int scare;
+    int p = rpeasants(u->region);
 
-    scare = rc_scare(rc);
-    if (scare>0) {
-        n = rng_int() % scare * u->number;
-    } else {
-        n = rng_int() % (u->number / 20 + 1);
-        horse = 0;
-    }
-    horse = horse ? i_get(u->items, rhorse->itype) : 0;
+    if (p > 0) {
+        int horse = -1;
+        int scare = rc_scare(rc);
+        int n = 0;
 
-    n = (int)(n * multi);
-    if (n > 0) {
-
-        n = lovar(n);
-
-        if (n > 0) {
-            int p = rpeasants(u->region);
-            if (p < n) n = p;
-            deathcounts(u->region, n);
-            rsetpeasants(u->region, rpeasants(u->region) - n);
-            ADDMSG(&u->region->msgs, msg_message("eatpeasants", "unit amount", u, n));
+        if (scare > 0) {
+            n = rng_int() % scare * u->number;
+        }
+        else {
+            n = rng_int() % (u->number / 20 + 1);
+            horse = 0;
+        }
+
+        horse = horse ? i_get(u->items, rhorse->itype) : 0;
+        if (horse > 0) {
+            i_change(&u->items, rhorse->itype, -horse);
+            ADDMSG(&u->region->msgs, msg_message("eathorse", "unit amount", u, horse));
+        }
+
+        n = (int)(n * multi);
+        if (n > 0) {
+            n = lovar(n);
+
+            if (p < n) n = p;
+            if (n > 0) {
+                if (n > 0) {
+                    deathcounts(u->region, n);
+                    rsetpeasants(u->region, rpeasants(u->region) - n);
+                    ADDMSG(&u->region->msgs, msg_message("eatpeasants", "unit amount", u, n));
+                }
+            }
         }
-    }
-    if (horse > 0) {
-        i_change(&u->items, rhorse->itype, -horse);
-        ADDMSG(&u->region->msgs, msg_message("eathorse", "unit amount", u, horse));
     }
 }
 
@@ -1059,10 +1064,12 @@ static void absorbed_by_monster(unit * u)
         if (n > 0) {
             int p = rpeasants(u->region);
             if (p < n) n = p;
-            rsetpeasants(u->region, rpeasants(u->region) - n);
-            scale_number(u, u->number + n);
-            ADDMSG(&u->region->msgs, msg_message("absorbpeasants",
-                "unit race amount", u, u_race(u), n));
+            if (n > 0) {
+                rsetpeasants(u->region, rpeasants(u->region) - n);
+                scale_number(u, u->number + n);
+                ADDMSG(&u->region->msgs, msg_message("absorbpeasants",
+                    "unit race amount", u, u_race(u), n));
+            }
         }
     }
 }

From 876113d991ea14be521c9f09eccf6e90a503bcf4 Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno.rehling@gmail.com>
Date: Mon, 3 Sep 2018 20:43:17 +0200
Subject: [PATCH 61/62] fix multiple coverity defects in parsers.

---
 src/exparse.c     | 442 ++++++++++++++++++++++++----------------------
 src/util/pofile.c |   9 +-
 2 files changed, 238 insertions(+), 213 deletions(-)

diff --git a/src/exparse.c b/src/exparse.c
index 9f49459a9..71efe0667 100644
--- a/src/exparse.c
+++ b/src/exparse.c
@@ -65,14 +65,17 @@ typedef struct parseinfo {
     void *object;
 } parseinfo;
 
-static int xml_strcmp(const XML_Char *xs, const char *cs) {
-    return strcmp(xs, cs);
+static bool xml_strequal(const XML_Char *xs, const char *cs) {
+    if (xs && cs) {
+        return strcmp(xs, cs) == 0;
+    }
+    return false;
 }
 
 static bool xml_bool(const XML_Char *val) {
-    if (xml_strcmp(val, "yes") == 0) return true;
-    if (xml_strcmp(val, "true") == 0) return true;
-    if (xml_strcmp(val, "1") == 0) return true;
+    if (xml_strequal(val, "yes")) return true;
+    if (xml_strequal(val, "true")) return true;
+    if (xml_strequal(val, "1")) return true;
     return false;
 }
 
@@ -94,7 +97,7 @@ static variant xml_fraction(const XML_Char *val) {
 const XML_Char *attr_get(const XML_Char **attr, const char *key) {
     int i;
     for (i = 0; attr[i]; i += 2) {
-        if (xml_strcmp(attr[i], key) == 0) {
+        if (xml_strequal(attr[i], key)) {
             return attr[i + 1];
         }
     }
@@ -134,7 +137,7 @@ static bool handle_flag(int *flags, const XML_Char **pair, const char *names[])
     for (i = 0; names[i]; ++i) {
         const char * name = names[i];
         if (name[0] == '!') {
-            if (xml_strcmp(pair[0], name+1) == 0) {
+            if (xml_strequal(pair[0], name+1)) {
                 if (xml_bool(pair[1])) {
                     *flags &= ~(1 << i);
                 }
@@ -144,7 +147,7 @@ static bool handle_flag(int *flags, const XML_Char **pair, const char *names[])
                 return true;
             }
         }
-        else if (xml_strcmp(pair[0], name) == 0) {
+        else if (xml_strequal(pair[0], name)) {
             if (xml_bool(pair[1])) {
                 *flags |= (1 << i);
             }
@@ -165,15 +168,15 @@ static void handle_resource(parseinfo *pi, const XML_Char *el, const XML_Char **
     bool material = false;
     (void)el;
     for (i = 0; attr[i]; i += 2) {
-        if (xml_strcmp(attr[i], "name") == 0) {
+        if (xml_strequal(attr[i], "name")) {
             name = attr[i + 1];
         }
-        else if (xml_strcmp(attr[i], "appearance") == 0) {
+        else if (xml_strequal(attr[i], "appearance")) {
             /* TODO: appearance should be a property of item, not resource */
             appear = attr[i + 1];
             flags |= RTF_ITEM;
         }
-        else if (xml_strcmp(attr[i], "material") == 0) {
+        else if (xml_strequal(attr[i], "material")) {
             material = xml_bool(attr[i + 1]);
         }
         else if (!handle_flag(&flags, attr + i, flag_names)) {
@@ -206,22 +209,22 @@ static void handle_item(parseinfo *pi, const XML_Char *el, const XML_Char **attr
     }
     for (i = 0; attr[i]; i += 2) {
         char buffer[64];
-        if (xml_strcmp(attr[i], "weight") == 0) {
+        if (xml_strequal(attr[i], "weight")) {
             itype->weight = xml_int(attr[i + 1]);
         }
-        else if (xml_strcmp(attr[i], "capacity") == 0) {
+        else if (xml_strequal(attr[i], "capacity")) {
             itype->capacity = xml_int(attr[i + 1]);
         }
-        else if (xml_strcmp(attr[i], "score") == 0) {
+        else if (xml_strequal(attr[i], "score")) {
             itype->score = xml_int(attr[i + 1]);
         }
-        else if (xml_strcmp(attr[i], "allow") == 0) {
+        else if (xml_strequal(attr[i], "allow")) {
             size_t len = strlen(attr[i + 1]);
             assert(len < sizeof(buffer));
             memcpy(buffer, attr[i + 1], len + 1);
             itype->mask_allow = rc_get_mask(buffer);
         }
-        else if (xml_strcmp(attr[i], "deny") == 0) {
+        else if (xml_strequal(attr[i], "deny")) {
             size_t len = strlen(attr[i + 1]);
             assert(len < sizeof(buffer));
             memcpy(buffer, attr[i + 1], len + 1);
@@ -244,16 +247,16 @@ static void handle_armor(parseinfo *pi, const XML_Char *el, const XML_Char **att
     armor_type *atype = new_armortype(itype, 0.0, frac_zero, 0, 0);
     int i, flags = 0;
     for (i = 0; attr[i]; i += 2) {
-        if (xml_strcmp(attr[i], "penalty") == 0) {
+        if (xml_strequal(attr[i], "penalty")) {
             atype->penalty = xml_float(attr[i + 1]);
         }
-        else if (xml_strcmp(attr[i], "projectile") == 0) {
+        else if (xml_strequal(attr[i], "projectile")) {
             atype->projectile = xml_float(attr[i + 1]);
         }
-        else if (xml_strcmp(attr[i], "ac") == 0) {
+        else if (xml_strequal(attr[i], "ac")) {
             atype->prot = xml_int(attr[i + 1]);
         }
-        else if (xml_strcmp(attr[i], "magres") == 0) {
+        else if (xml_strequal(attr[i], "magres")) {
             atype->magres = xml_fraction(attr[i + 1]);
         }
         else if (!handle_flag(&flags, attr + i, flag_names)) {
@@ -270,19 +273,19 @@ static void handle_weapon(parseinfo *pi, const XML_Char *el, const XML_Char **at
     weapon_type *wtype = new_weapontype(itype, 0, frac_zero, NULL, 0, 0, 0, NOSKILL);
     int i, flags = 0;
     for (i = 0; attr[i]; i += 2) {
-        if (xml_strcmp(attr[i], "offmod") == 0) {
+        if (xml_strequal(attr[i], "offmod")) {
             wtype->offmod = xml_int(attr[i + 1]);
         }
-        else if (xml_strcmp(attr[i], "defmod") == 0) {
+        else if (xml_strequal(attr[i], "defmod")) {
             wtype->defmod = xml_int(attr[i + 1]);
         }
-        else if (xml_strcmp(attr[i], "reload") == 0) {
+        else if (xml_strequal(attr[i], "reload")) {
             wtype->reload = xml_int(attr[i + 1]);
         }
-        else if (xml_strcmp(attr[i], "skill") == 0) {
+        else if (xml_strequal(attr[i], "skill")) {
             wtype->skill = findskill(attr[i + 1]);
         }
-        else if (xml_strcmp(attr[i], "magres") == 0) {
+        else if (xml_strequal(attr[i], "magres")) {
             wtype->magres = xml_fraction(attr[i + 1]);;
         }
         else if (!handle_flag(&flags, attr + i, flag_names)) {
@@ -296,7 +299,7 @@ static int msg_nargs;
 static char * msg_args[MSG_MAXARGS];
 
 static void end_messages(parseinfo *pi, const XML_Char *el) {
-    if (xml_strcmp(el, "message") == 0) {
+    if (xml_strequal(el, "message")) {
         int i;
         struct message_type *mtype = (struct message_type *)pi->object;
         assert(mtype);
@@ -312,23 +315,23 @@ static void end_messages(parseinfo *pi, const XML_Char *el) {
         }
         msg_nargs = 0;
     }
-    else if (xml_strcmp(el, "messages") == 0) {
+    else if (xml_strequal(el, "messages")) {
         pi->type = EXP_UNKNOWN;
     }
 }
 
 static void start_messages(parseinfo *pi, const XML_Char *el, const XML_Char **attr) {
-    if (xml_strcmp(el, "arg") == 0) {
+    if (xml_strequal(el, "arg")) {
         int i;
         const XML_Char *name = NULL, *type = NULL;
 
         assert(msg_nargs < MSG_MAXARGS);
         for (i = 0; attr[i]; i += 2) {
             const XML_Char *key = attr[i], *val = attr[i + 1];
-            if (xml_strcmp(key, "name") == 0) {
+            if (xml_strequal(key, "name")) {
                 name = val;
             }
-            else if (xml_strcmp(key, "type") == 0) {
+            else if (xml_strequal(key, "type")) {
                 type = val;
             }
             else {
@@ -341,15 +344,15 @@ static void start_messages(parseinfo *pi, const XML_Char *el, const XML_Char **a
             msg_args[msg_nargs++] = str_strdup(zBuffer);
         }
     }
-    else if (xml_strcmp(el, "message") == 0) {
+    else if (xml_strequal(el, "message")) {
         const XML_Char *name = NULL, *section = NULL;
         int i;
         for (i = 0; attr[i]; i += 2) {
             const XML_Char *key = attr[i], *val = attr[i + 1];
-            if (xml_strcmp(key, "name") == 0) {
+            if (xml_strequal(key, "name")) {
                 name = val;
             }
-            else if (xml_strcmp(key, "section") == 0) {
+            else if (xml_strequal(key, "section")) {
                 section = val;
             }
             else {
@@ -360,7 +363,7 @@ static void start_messages(parseinfo *pi, const XML_Char *el, const XML_Char **a
             pi->object = mt_new(name, section);
         }
     }
-    else if (xml_strcmp(el, "type") != 0) {
+    else if (!xml_strequal(el, "type")) {
         handle_bad_input(pi, el, NULL);
     }
 }
@@ -374,7 +377,7 @@ static void start_spells(parseinfo *pi, const XML_Char *el, const XML_Char **att
         "far", "variable", "ocean", "ship", "los", 
         "unittarget", "shiptarget", "buildingtarget", "regiontarget", "globaltarget", NULL };
 
-    if (xml_strcmp(el, "resource") == 0) {
+    if (xml_strequal(el, "resource")) {
         spell_component *spc;
         int i;
 
@@ -385,20 +388,20 @@ static void start_spells(parseinfo *pi, const XML_Char *el, const XML_Char **att
         memset(spc, 0, sizeof(spell_component));
         for (i = 0; attr[i]; i += 2) {
             const XML_Char *key = attr[i], *val = attr[i + 1];
-            if (xml_strcmp(key, "name") == 0) {
+            if (xml_strequal(key, "name")) {
                 spc->type = rt_get_or_create(val);
             }
-            else if (xml_strcmp(key, "amount") == 0) {
+            else if (xml_strequal(key, "amount")) {
                 spc->amount = xml_int(val);
             }
-            else if (xml_strcmp(key, "cost") == 0) {
-                if (xml_strcmp(val, "level") == 0) {
+            else if (xml_strequal(key, "cost")) {
+                if (xml_strequal(val, "level")) {
                     spc->cost = SPC_LEVEL;
                 }
-                else if (xml_strcmp(val, "linear") == 0) {
+                else if (xml_strequal(val, "linear")) {
                     spc->cost = SPC_LINEAR;
                 }
-                else if (xml_strcmp(val, "fixed") == 0) {
+                else if (xml_strequal(val, "fixed")) {
                     spc->cost = SPC_FIX;
                 }
                 else {
@@ -410,25 +413,25 @@ static void start_spells(parseinfo *pi, const XML_Char *el, const XML_Char **att
             }
         }
     }
-    else if (xml_strcmp(el, "spell") == 0) {
+    else if (xml_strequal(el, "spell")) {
         spell *sp;
         const XML_Char *name = NULL, *syntax = NULL, *parameter = NULL;
         int i, rank = 0, flags = 0;
         for (i = 0; attr[i]; i += 2) {
             const XML_Char *key = attr[i], *val = attr[i + 1];
-            if (xml_strcmp(key, "name") == 0) {
+            if (xml_strequal(key, "name")) {
                 name = val;
             }
-            else if (xml_strcmp(key, "syntax") == 0) {
+            else if (xml_strequal(key, "syntax")) {
                 syntax = val;
             }
-            else if (xml_strcmp(key, "parameters") == 0) {
+            else if (xml_strequal(key, "parameters")) {
                 parameter = val;
             }
-            else if (xml_strcmp(key, "rank") == 0) {
+            else if (xml_strequal(key, "rank")) {
                 rank = xml_int(val);
             }
-            else if (xml_strcmp(key, "combat") == 0) {
+            else if (xml_strequal(key, "combat")) {
                 int mode = PRECOMBATSPELL;
                 int k = xml_int(val);
                 if (k > 1 && k <= 3) {
@@ -453,7 +456,7 @@ static void start_spells(parseinfo *pi, const XML_Char *el, const XML_Char **att
 
 static void start_spellbooks(parseinfo *pi, const XML_Char *el, const XML_Char **attr) {
     spellbook * sb = (spellbook *)pi->object;
-    if (xml_strcmp(el, "spellbook") == 0) {
+    if (xml_strequal(el, "spellbook")) {
         const XML_Char *name = attr_get(attr, "name");
 
         if (name) {
@@ -463,16 +466,16 @@ static void start_spellbooks(parseinfo *pi, const XML_Char *el, const XML_Char *
             handle_bad_input(pi, el, NULL);
         }
     }
-    else if (xml_strcmp(el, "entry") == 0) {
+    else if (xml_strequal(el, "entry")) {
         int i, level = 0;
         const XML_Char *name = NULL;
 
         assert(sb);
         for (i = 0; attr[i]; i += 2) {
-            if (xml_strcmp(attr[i], "spell") == 0) {
+            if (xml_strequal(attr[i], "spell")) {
                 name = attr[i + 1];
             }
-            else if (xml_strcmp(attr[i], "level") == 0) {
+            else if (xml_strequal(attr[i], "level")) {
                 level = xml_int(attr[i + 1]);
             }
             else {
@@ -495,22 +498,22 @@ static void start_weapon(parseinfo *pi, const XML_Char *el, const XML_Char **att
     resource_type *rtype = (resource_type *)pi->object;
 
     assert(rtype && rtype->wtype);
-    if (xml_strcmp(el, "function") == 0) {
+    if (xml_strequal(el, "function")) {
         const XML_Char *name = NULL, *type = NULL;
         int i;
 
         for (i = 0; attr[i]; i += 2) {
-            if (xml_strcmp(attr[i], "name") == 0) {
+            if (xml_strequal(attr[i], "name")) {
                 type = attr[i + 1];
             }
-            else if (xml_strcmp(attr[i], "value") == 0) {
+            else if (xml_strequal(attr[i], "value")) {
                 name = attr[i + 1];
             }
             else {
                 handle_bad_input(pi, el, attr[i]);
             }
         }
-        if (type && xml_strcmp(type, "attack") == 0) {
+        if (name && type && xml_strequal(type, "attack")) {
             pf_generic fun = get_function(name);
             rtype->wtype->attack = (wtype_attack)fun;
         }
@@ -518,49 +521,49 @@ static void start_weapon(parseinfo *pi, const XML_Char *el, const XML_Char **att
             handle_bad_input(pi, el, attr[i]);
         }
     }
-    else if (xml_strcmp(el, "modifier") == 0) {
+    else if (xml_strequal(el, "modifier")) {
         const XML_Char *type = NULL;
         int i, flags = 0, race_mask = 0;
         int value = 0;
 
         for (i = 0; attr[i]; i += 2) {
-            if (xml_strcmp(attr[i], "type") == 0) {
+            if (xml_strequal(attr[i], "type")) {
                 type = attr[i + 1];
             }
-            else if (xml_strcmp(attr[i], "value") == 0) {
+            else if (xml_strequal(attr[i], "value")) {
                 value = xml_int(attr[i + 1]);
             }
-            else if (xml_strcmp(attr[i], "races") == 0) {
+            else if (xml_strequal(attr[i], "races")) {
                 char list[64];
-                strcpy(list, attr[i + 1]);
+                str_strlcpy(list, attr[i + 1], sizeof(list));
                 race_mask = rc_get_mask(list);
             }
-            else if (xml_strcmp(attr[i], "offensive") == 0) {
+            else if (xml_strequal(attr[i], "offensive")) {
                 if (xml_bool(attr[i + 1])) {
                     flags |= WMF_OFFENSIVE;
                 }
             }
-            else if (xml_strcmp(attr[i], "defensive") == 0) {
+            else if (xml_strequal(attr[i], "defensive")) {
                 if (xml_bool(attr[i + 1])) {
                     flags |= WMF_DEFENSIVE;
                 }
             }
-            else if (xml_strcmp(attr[i], "walking") == 0) {
+            else if (xml_strequal(attr[i], "walking")) {
                 if (xml_bool(attr[i + 1])) {
                     flags |= WMF_WALKING;
                 }
             }
-            else if (xml_strcmp(attr[i], "riding") == 0) {
+            else if (xml_strequal(attr[i], "riding")) {
                 if (xml_bool(attr[i + 1])) {
                     flags |= WMF_RIDING;
                 }
             }
-            else if (xml_strcmp(attr[i], "against_riding") == 0) {
+            else if (xml_strequal(attr[i], "against_riding")) {
                 if (xml_bool(attr[i + 1])) {
                     flags |= WMF_AGAINST_RIDING;
                 }
             }
-            else if (xml_strcmp(attr[i], "against_walking") == 0) {
+            else if (xml_strequal(attr[i], "against_walking")) {
                 if (xml_bool(attr[i + 1])) {
                     flags |= WMF_AGAINST_WALKING;
                 }
@@ -576,13 +579,13 @@ static void start_weapon(parseinfo *pi, const XML_Char *el, const XML_Char **att
             ++nwmods;
 
             /* weapon modifiers */
-            if (xml_strcmp(type, "missile_target") == 0) {
+            if (xml_strequal(type, "missile_target")) {
                 flags |= WMF_MISSILE_TARGET;
             }
-            else if (xml_strcmp(type, "damage") == 0) {
+            else if (xml_strequal(type, "damage")) {
                 flags |= WMF_DAMAGE;
             }
-            else if (xml_strcmp(type, "skill") == 0) {
+            else if (xml_strequal(type, "skill")) {
                 flags |= WMF_SKILL;
             }
             else {
@@ -596,17 +599,17 @@ static void start_weapon(parseinfo *pi, const XML_Char *el, const XML_Char **att
             handle_bad_input(pi, el, NULL);
         }
     }
-    else if (xml_strcmp(el, "damage") == 0) {
+    else if (xml_strequal(el, "damage")) {
         weapon_type *wtype = rtype->wtype;
         int i, pos = 0;
         for (i = 0; attr[i]; i += 2) {
-            if (xml_strcmp(attr[i], "type") == 0) {
+            if (xml_strequal(attr[i], "type")) {
                 /* damage vs. rider(1) or not(0)? */
-                if (xml_strcmp(attr[i + 1], "rider") == 0) {
+                if (xml_strequal(attr[i + 1], "rider")) {
                     pos = 1;
                 }
             }
-            else if (xml_strcmp(attr[i], "value") == 0) {
+            else if (xml_strequal(attr[i], "value")) {
                 wtype->damage[pos] = str_strdup(attr[i + 1]);
             }
             else {
@@ -627,10 +630,10 @@ static void handle_requirement(parseinfo *pi, const XML_Char *el, const XML_Char
     req = reqs + nreqs;
     req->number = 1;
     for (i = 0; attr[i]; i += 2) {
-        if (xml_strcmp(attr[i], "type") == 0) {
+        if (xml_strequal(attr[i], "type")) {
             req->rtype = rt_get_or_create(attr[i + 1]);
         }
-        else if (xml_strcmp(attr[i], "quantity") == 0) {
+        else if (xml_strequal(attr[i], "quantity")) {
             req->number = xml_int(attr[i + 1]);
         }
         else {
@@ -648,13 +651,13 @@ static void handle_maintenance(parseinfo *pi, const XML_Char *el, const XML_Char
     up = upkeep + nupkeep;
     memset(up, 0, sizeof(maintenance));
     for (i = 0; attr[i]; i += 2) {
-        if (xml_strcmp(attr[i], "type") == 0) {
+        if (xml_strequal(attr[i], "type")) {
             up->rtype = rt_get_or_create(attr[i + 1]);
         }
-        else if (xml_strcmp(attr[i], "amount") == 0) {
+        else if (xml_strequal(attr[i], "amount")) {
             up->number = xml_int(attr[i + 1]);
         }
-        else if (xml_strcmp(attr[i], "variable") == 0) {
+        else if (xml_strequal(attr[i], "variable")) {
             if (xml_bool(attr[i + 1])) {
                 up->flags |= MTF_VARIABLE;
             }
@@ -692,42 +695,57 @@ static void handle_modifier(parseinfo *pi, const XML_Char *el, const XML_Char **
     assert(nrmods < RMOD_MAX);
     ++nrmods;
     for (i = 0; attr[i]; i += 2) {
-        if (xml_strcmp(attr[i], "type") == 0) {
+        if (xml_strequal(attr[i], "type")) {
             type = attr[i + 1];
         }
-        else if (xml_strcmp(attr[i], "building") == 0) {
+        else if (xml_strequal(attr[i], "building")) {
             mod->btype = bt_get_or_create(attr[i + 1]);
         }
-        else if (xml_strcmp(attr[i], "skill") == 0) {
+        else if (xml_strequal(attr[i], "skill")) {
             sk = findskill(attr[i + 1]);
         }
-        else if (xml_strcmp(attr[i], "races") == 0) {
+        else if (xml_strequal(attr[i], "races")) {
             char list[64];
-            strcpy(list, attr[i + 1]);
+            str_strlcpy(list, attr[i + 1], sizeof(list));
             mod->race_mask = rc_get_mask(list);
         }
-        else if (xml_strcmp(attr[i], "value") == 0) {
+        else if (xml_strequal(attr[i], "value")) {
             value = attr[i + 1];
         }
         else {
             handle_bad_input(pi, el, attr[i]);
         }
     }
-    if (xml_strcmp(type, "skill") == 0) {
+    if (xml_strequal(type, "skill")) {
         mod->type = RMT_PROD_SKILL;
-        mod->value.sa[0] = (short)sk;
-        mod->value.sa[1] = (short)xml_int(value);
+        if (value) {
+            mod->value.sa[0] = (short)sk;
+            mod->value.sa[1] = (short)xml_int(value);
+        }
+        else {
+            handle_bad_input(pi, el, type);
+        }
     }
-    else if (xml_strcmp(type, "require") == 0) {
+    else if (xml_strequal(type, "require")) {
         mod->type = RMT_PROD_REQUIRE;
     }
-    else if (xml_strcmp(type, "material") == 0) {
+    else if (xml_strequal(type, "material")) {
         mod->type = RMT_PROD_SAVE;
-        mod->value = xml_fraction(value);
+        if (value) {
+            mod->value = xml_fraction(value);
+        }
+        else {
+            handle_bad_input(pi, el, type);
+        }
     }
-    else if (xml_strcmp(type, "save") == 0) {
+    else if (xml_strequal(type, "save")) {
         mod->type = RMT_USE_SAVE;
-        mod->value = xml_fraction(value);
+        if (value) {
+            mod->value = xml_fraction(value);
+        }
+        else {
+            handle_bad_input(pi, el, type);
+        }
     }
     else {
         handle_bad_input(pi, el, type);
@@ -741,19 +759,19 @@ static construction *parse_construction(parseinfo *pi, const XML_Char *el, const
     con->minskill = -1;
     con->reqsize = 1;
     for (i = 0; attr[i]; i += 2) {
-        if (xml_strcmp(attr[i], "skill") == 0) {
+        if (xml_strequal(attr[i], "skill")) {
             con->skill = findskill(attr[i + 1]);
         }
-        else if (xml_strcmp(attr[i], "maxsize") == 0) {
+        else if (xml_strequal(attr[i], "maxsize")) {
             con->maxsize = xml_int(attr[i + 1]);
         }
-        else if (xml_strcmp(attr[i], "reqsize") == 0) {
+        else if (xml_strequal(attr[i], "reqsize")) {
             con->reqsize = xml_int(attr[i + 1]);
         }
-        else if (xml_strcmp(attr[i], "minskill") == 0) {
+        else if (xml_strequal(attr[i], "minskill")) {
             con->minskill = xml_int(attr[i + 1]);
         }
-        else if (stage != NULL && xml_strcmp(attr[i], "name") == 0) {
+        else if (stage != NULL && xml_strequal(attr[i], "name")) {
             /* only building stages have names */
             stage->name = str_strdup(attr[i + 1]);
         }
@@ -767,24 +785,24 @@ static construction *parse_construction(parseinfo *pi, const XML_Char *el, const
 
 static void start_resources(parseinfo *pi, const XML_Char *el, const XML_Char **attr) {
     resource_type *rtype = (resource_type *)pi->object;
-    if (xml_strcmp(el, "resource") == 0) {
+    if (xml_strequal(el, "resource")) {
         handle_resource(pi, el, attr);
     }
     else if (rtype) {
-        if (xml_strcmp(el, "item") == 0) {
+        if (xml_strequal(el, "item")) {
             assert(rtype);
             handle_item(pi, el, attr);
         }
-        else if (xml_strcmp(el, "function") == 0) {
+        else if (xml_strequal(el, "function")) {
             const XML_Char *name = NULL;
             pf_generic fun = NULL;
             int i;
 
             for (i = 0; attr[i]; i += 2) {
-                if (xml_strcmp(attr[i], "name") == 0) {
+                if (xml_strequal(attr[i], "name")) {
                     name = attr[i + 1];
                 }
-                else if (xml_strcmp(attr[i], "value") == 0) {
+                else if (xml_strequal(attr[i], "value")) {
                     fun = get_function(attr[i + 1]);
                 }
                 else {
@@ -794,39 +812,39 @@ static void start_resources(parseinfo *pi, const XML_Char *el, const XML_Char **
 
             assert(rtype);
             if (name && fun) {
-                if (xml_strcmp(name, "change") == 0) {
+                if (xml_strequal(name, "change")) {
                     rtype->uchange = (rtype_uchange)fun;
                 }
-                else if (xml_strcmp(name, "name") == 0) {
+                else if (xml_strequal(name, "name")) {
                     rtype->name = (rtype_name)fun;
                 }
-                else if (xml_strcmp(name, "attack") == 0) {
+                else if (xml_strequal(name, "attack")) {
                     assert(rtype->wtype);
                     rtype->wtype->attack = (wtype_attack)fun;
                 }
             }
         }
-        else if (xml_strcmp(el, "modifier") == 0) {
+        else if (xml_strequal(el, "modifier")) {
             handle_modifier(pi, el, attr);
         }
         else if (rtype->itype) {
             item_type *itype = rtype->itype;
-            if (xml_strcmp(el, "construction") == 0) {
+            if (xml_strequal(el, "construction")) {
                 itype->construction = parse_construction(pi, el, attr);
             }
-            else if (xml_strcmp(el, "requirement") == 0) {
+            else if (xml_strequal(el, "requirement")) {
                 assert(itype->construction);
                 handle_requirement(pi, el, attr);
             }
-            else if (xml_strcmp(el, "luxury") == 0) {
+            else if (xml_strequal(el, "luxury")) {
                 int price = atoi(attr_get(attr, "price"));
                 assert(price > 0);
                 rtype->ltype = new_luxurytype(itype, price);
             }
-            else if (xml_strcmp(el, "potion") == 0) {
+            else if (xml_strequal(el, "potion")) {
                 int i, level = 0;
                 for (i = 0; attr[i]; i += 2) {
-                    if (xml_strcmp(attr[i], "level") == 0) {
+                    if (xml_strequal(attr[i], "level")) {
                         level = xml_int(attr[i + 1]);
                     }
                     else {
@@ -835,10 +853,10 @@ static void start_resources(parseinfo *pi, const XML_Char *el, const XML_Char **
                 }
                 new_potiontype(itype, level);
             }
-            else if (xml_strcmp(el, "armor") == 0) {
+            else if (xml_strequal(el, "armor")) {
                 handle_armor(pi, el, attr);
             }
-            else if (xml_strcmp(el, "weapon") == 0) {
+            else if (xml_strequal(el, "weapon")) {
                 pi->type = EXP_WEAPON;
                 handle_weapon(pi, el, attr);
             }
@@ -857,7 +875,7 @@ static void start_resources(parseinfo *pi, const XML_Char *el, const XML_Char **
 
 static void start_ships(parseinfo *pi, const XML_Char *el, const XML_Char **attr) {
     const char *flag_names[] = { "opensea", "fly", "nocoast", "speedy", NULL };
-    if (xml_strcmp(el, "ship") == 0) {
+    if (xml_strequal(el, "ship")) {
         const XML_Char *name;
 
         name = attr_get(attr, "name");
@@ -866,42 +884,42 @@ static void start_ships(parseinfo *pi, const XML_Char *el, const XML_Char **attr
             int i, flags = SFL_DEFAULT;
 
             for (i = 0; attr[i]; i += 2) {
-                if (xml_strcmp(attr[i], "range") == 0) {
+                if (xml_strequal(attr[i], "range")) {
                     stype->range = xml_int(attr[i + 1]);
                 }
-                else if (xml_strcmp(attr[i], "maxrange") == 0) {
+                else if (xml_strequal(attr[i], "maxrange")) {
                     stype->range_max = xml_int(attr[i + 1]);
                 }
-                else if (xml_strcmp(attr[i], "cabins") == 0) {
+                else if (xml_strequal(attr[i], "cabins")) {
                     stype->cabins = PERSON_WEIGHT * xml_int(attr[i + 1]);
                 }
-                else if (xml_strcmp(attr[i], "cargo") == 0) {
+                else if (xml_strequal(attr[i], "cargo")) {
                     stype->cargo = xml_int(attr[i + 1]);
                 }
-                else if (xml_strcmp(attr[i], "combat") == 0) {
+                else if (xml_strequal(attr[i], "combat")) {
                     stype->combat = xml_int(attr[i + 1]);
                 }
-                else if (xml_strcmp(attr[i], "fishing") == 0) {
+                else if (xml_strequal(attr[i], "fishing")) {
                     stype->fishing = xml_int(attr[i + 1]);
                 }
-                else if (xml_strcmp(attr[i], "cptskill") == 0) {
+                else if (xml_strequal(attr[i], "cptskill")) {
                     stype->cptskill = xml_int(attr[i + 1]);
                 }
-                else if (xml_strcmp(attr[i], "minskill") == 0) {
+                else if (xml_strequal(attr[i], "minskill")) {
                     stype->minskill = xml_int(attr[i + 1]);
                 }
-                else if (xml_strcmp(attr[i], "sumskill") == 0) {
+                else if (xml_strequal(attr[i], "sumskill")) {
                     stype->sumskill = xml_int(attr[i + 1]);
                 }
-                else if (xml_strcmp(attr[i], "damage") == 0) {
+                else if (xml_strequal(attr[i], "damage")) {
                     stype->damage = xml_float(attr[i + 1]);
                 }
-                else if (xml_strcmp(attr[i], "storm") == 0) {
+                else if (xml_strequal(attr[i], "storm")) {
                     stype->storm = xml_float(attr[i + 1]);
                 }
                 else if (!handle_flag(&flags, attr + i, flag_names)) {
                     /* we already handled the name earlier */
-                    if (xml_strcmp(attr[i], "name") != 0) {
+                    if (!xml_strequal(attr[i], "name")) {
                         handle_bad_input(pi, el, attr[i]);
                     }
                 }
@@ -913,18 +931,18 @@ static void start_ships(parseinfo *pi, const XML_Char *el, const XML_Char **attr
     else {
         ship_type *stype = (ship_type *)pi->object;
         assert(stype);
-        if (xml_strcmp(el, "modifier") == 0) {
+        if (xml_strequal(el, "modifier")) {
             /* these modifiers are not like buildings */
             int i;
             const XML_Char *type = NULL, *value = NULL;
             for (i = 0; attr[i]; i += 2) {
-                if (xml_strcmp(attr[i], "type") == 0) {
+                if (xml_strequal(attr[i], "type")) {
                     type = attr[i + 1];
                 }
-                else if (xml_strcmp(attr[i], "value") == 0) {
+                else if (xml_strequal(attr[i], "value")) {
                     value = attr[i + 1];
                 }
-                else if (xml_strcmp(attr[i], "factor") == 0) {
+                else if (xml_strequal(attr[i], "factor")) {
                     value = attr[i + 1];
                 }
                 else {
@@ -932,26 +950,32 @@ static void start_ships(parseinfo *pi, const XML_Char *el, const XML_Char **attr
                 }
             }
             if (type) {
-                if (xml_strcmp(type, "tactics") == 0) {
+                if (!value) {
+                    handle_bad_input(pi, el, attr[i]);
+                }
+                else if (xml_strequal(type, "tactics")) {
                     stype->tac_bonus = xml_float(value);
                 }
-                else if (xml_strcmp(type, "attack") == 0) {
+                else if (xml_strequal(type, "attack")) {
                     stype->at_bonus = xml_int(value);
                 }
-                else if (xml_strcmp(type, "defense") == 0) {
+                else if (xml_strequal(type, "defense")) {
                     stype->df_bonus = xml_int(value);
                 }
+                else {
+                    handle_bad_input(pi, el, attr[i]);
+                }
             }
         }
-        else if (xml_strcmp(el, "requirement") == 0) {
+        else if (xml_strequal(el, "requirement")) {
             assert(stype->construction);
             handle_requirement(pi, el, attr);
         }
-        else if (xml_strcmp(el, "construction") == 0) {
+        else if (xml_strequal(el, "construction")) {
             assert(!stype->construction);
             stype->construction = parse_construction(pi, el, attr);
         }
-        else if (xml_strcmp(el, "coast") == 0) {
+        else if (xml_strequal(el, "coast")) {
             handle_coast(pi, el, attr);
         }
         else {
@@ -979,7 +1003,7 @@ static void start_races(parseinfo *pi, const XML_Char *el, const XML_Char **attr
         "giveperson", "giveunit", "getitem", "recruitethereal", 
         "recruitunlimited", "stonegolem", "irongolem", NULL };
 
-    if (xml_strcmp(el, "attack") == 0) {
+    if (xml_strequal(el, "attack")) {
         int i;
         struct att * at;
         assert(rc);
@@ -991,19 +1015,19 @@ static void start_races(parseinfo *pi, const XML_Char *el, const XML_Char **attr
         }
         for (i = 0; attr[i]; i += 2) {
             const XML_Char *key = attr[i], *val = attr[i + 1];
-            if (xml_strcmp(key, "type") == 0) {
+            if (xml_strequal(key, "type")) {
                 at->type = xml_int(val);
             }
-            else if (xml_strcmp(key, "flags") == 0) {
+            else if (xml_strequal(key, "flags")) {
                 at->flags = xml_int(val);
             }
-            else if (xml_strcmp(key, "level") == 0) {
+            else if (xml_strequal(key, "level")) {
                 at->level = xml_int(val);
             }
-            else if (xml_strcmp(key, "damage") == 0) {
+            else if (xml_strequal(key, "damage")) {
                 at->data.dice = str_strdup(val);
             }
-            else if (xml_strcmp(key, "spell") == 0) {
+            else if (xml_strequal(key, "spell")) {
                 at->data.sp = spellref_create(NULL, val);
             }
             else {
@@ -1011,14 +1035,14 @@ static void start_races(parseinfo *pi, const XML_Char *el, const XML_Char **attr
             }
         }
     }
-    else if (xml_strcmp(el, "familiar") == 0) {
+    else if (xml_strequal(el, "familiar")) {
         race *frc = NULL;
         int i;
 
         assert(rc);
         for (i = 0; attr[i]; i += 2) {
             const XML_Char *key = attr[i], *val = attr[i + 1];
-            if (xml_strcmp(key, "race") == 0) {
+            if (xml_strequal(key, "race")) {
                 frc = rc_get_or_create(val);
                 frc->flags |= RCF_FAMILIAR;
             }
@@ -1032,19 +1056,19 @@ static void start_races(parseinfo *pi, const XML_Char *el, const XML_Char **attr
             }
         }
     }
-    else if (xml_strcmp(el, "skill") == 0) {
+    else if (xml_strequal(el, "skill")) {
         const XML_Char *name = NULL;
         int i, speed = 0, mod = 0;
 
         for (i = 0; attr[i]; i += 2) {
             const XML_Char *key = attr[i], *val = attr[i + 1];
-            if (xml_strcmp(key, "name") == 0) {
+            if (xml_strequal(key, "name")) {
                 name = val;
             }
-            else if (xml_strcmp(key, "modifier") == 0) {
+            else if (xml_strequal(key, "modifier")) {
                 mod = xml_int(val);
             }
-            else if (xml_strcmp(key, "speed") == 0) {
+            else if (xml_strequal(key, "speed")) {
                 speed = xml_int(val);
             }
             else {
@@ -1061,22 +1085,22 @@ static void start_races(parseinfo *pi, const XML_Char *el, const XML_Char **attr
             }
         }
     }
-    else if (xml_strcmp(el, "param") == 0) {
+    else if (xml_strequal(el, "param")) {
         const XML_Char *key = attr_get(attr, "name"), *val = attr_get(attr, "value");
         if (key && val) {
             rc_set_param(rc, key, val);
         }
     }
-    else if (xml_strcmp(el, "ai") == 0) {
+    else if (xml_strequal(el, "ai")) {
         /* AI flags are cumulative to race flags. XML format is dumb */
         int i, flags = 0;
         assert(rc);
         for (i = 0; attr[i]; i += 2) {
             const XML_Char *key = attr[i], *val = attr[i + 1];
-            if (xml_strcmp(key, "splitsize") == 0) {
+            if (xml_strequal(key, "splitsize")) {
                 rc->splitsize = xml_int(val);
             }
-            else if (xml_strcmp(key, "scare") == 0) {
+            else if (xml_strequal(key, "scare")) {
                 rc_set_param(rc, "scare", val);
             }
             else if (!handle_flag(&flags, attr + i, flag_names)) {
@@ -1085,7 +1109,7 @@ static void start_races(parseinfo *pi, const XML_Char *el, const XML_Char **attr
         }
         rc->flags |= flags;
     }
-    else if (xml_strcmp(el, "race") == 0) {
+    else if (xml_strequal(el, "race")) {
         const XML_Char *name;
 
         nfamiliars = 0;
@@ -1103,59 +1127,59 @@ static void start_races(parseinfo *pi, const XML_Char *el, const XML_Char **attr
 
             for (i = 0; attr[i]; i += 2) {
                 const XML_Char *key = attr[i], *val = attr[i + 1];
-                if (xml_strcmp(key, "maxaura") == 0) {
+                if (xml_strequal(key, "maxaura")) {
                     rc->maxaura = (int)(100 * xml_float(val));
                 }
-                else if (xml_strcmp(key, "magres") == 0) {
+                else if (xml_strequal(key, "magres")) {
                     /* specified in percent: */
                     rc->magres = frac_make(xml_int(val), 100);
                 }
-                else if (xml_strcmp(key, "healing") == 0) {
+                else if (xml_strequal(key, "healing")) {
                     rc->healing = (int)(xml_float(val) * 100);
                 }
-                else if (xml_strcmp(key, "regaura") == 0) {
+                else if (xml_strequal(key, "regaura")) {
                     rc->regaura = xml_float(val);
                 }
-                else if (xml_strcmp(key, "recruitcost") == 0) {
+                else if (xml_strequal(key, "recruitcost")) {
                     rc->recruitcost = xml_int(val);
                 }
-                else if (xml_strcmp(key, "maintenance") == 0) {
+                else if (xml_strequal(key, "maintenance")) {
                     rc->maintenance = xml_int(val);
                 }
-                else if (xml_strcmp(key, "income") == 0) {
+                else if (xml_strequal(key, "income")) {
                     rc->income = xml_int(val);
                 }
-                else if (xml_strcmp(key, "weight") == 0) {
+                else if (xml_strequal(key, "weight")) {
                     rc->weight = xml_int(val);
                 }
-                else if (xml_strcmp(key, "capacity") == 0) {
+                else if (xml_strequal(key, "capacity")) {
                     rc->capacity = xml_int(val);
                 }
-                else if (xml_strcmp(key, "speed") == 0) {
+                else if (xml_strequal(key, "speed")) {
                     rc->speed = xml_float(val);
                 }
-                else if (xml_strcmp(key, "hp") == 0) {
+                else if (xml_strequal(key, "hp")) {
                     rc->hitpoints = xml_int(val);
                 }
-                else if (xml_strcmp(key, "ac") == 0) {
+                else if (xml_strequal(key, "ac")) {
                     rc->armor = xml_int(val);
                 }
-                else if (xml_strcmp(key, "damage") == 0) {
+                else if (xml_strequal(key, "damage")) {
                     rc->def_damage = str_strdup(val);
                 }
-                else if (xml_strcmp(key, "unarmedattack") == 0) {
+                else if (xml_strequal(key, "unarmedattack")) {
                     rc->at_default = xml_int(val);
                 }
-                else if (xml_strcmp(key, "unarmeddefense") == 0) {
+                else if (xml_strequal(key, "unarmeddefense")) {
                     rc->df_default = xml_int(val);
                 }
-                else if (xml_strcmp(key, "attackmodifier") == 0) {
+                else if (xml_strequal(key, "attackmodifier")) {
                     rc->at_bonus = xml_int(val);
                 }
-                else if (xml_strcmp(key, "defensemodifier") == 0) {
+                else if (xml_strequal(key, "defensemodifier")) {
                     rc->df_bonus = xml_int(val);
                 }
-                else if (xml_strcmp(key, "studyspeed") == 0) {
+                else if (xml_strequal(key, "studyspeed")) {
                     int study_speed = xml_int(val);
                     if (study_speed != 0) {
                         skill_t sk;
@@ -1169,7 +1193,7 @@ static void start_races(parseinfo *pi, const XML_Char *el, const XML_Char **attr
                     if (!handle_flag(&rc->battle_flags, attr + i, bflag_names)) {
                         if (!handle_flag(&rc->ec_flags, attr + i, eflag_names)) {
                             /* we already handled the name earlier: */
-                            if (xml_strcmp(key, "name") != 0) {
+                            if (!xml_strequal(key, "name")) {
                                 handle_bad_input(pi, el, attr[i]);
                             }
                         }
@@ -1186,7 +1210,7 @@ static void start_races(parseinfo *pi, const XML_Char *el, const XML_Char **attr
 
 static void start_buildings(parseinfo *pi, const XML_Char *el, const XML_Char **attr) {
     const char *flag_names[] = { "nodestroy", "nobuild", "unique", "decay", "magic", "namechange", "fort", "oneperturn", NULL };
-    if (xml_strcmp(el, "building") == 0) {
+    if (xml_strequal(el, "building")) {
         const XML_Char *name;
 
         assert(stage == NULL);
@@ -1196,34 +1220,34 @@ static void start_buildings(parseinfo *pi, const XML_Char *el, const XML_Char **
             int i, flags = BTF_DEFAULT;
 
             for (i = 0; attr[i]; i += 2) {
-                if (xml_strcmp(attr[i], "maxsize") == 0) {
+                if (xml_strequal(attr[i], "maxsize")) {
                     btype->maxsize = xml_int(attr[i + 1]);
                 }
-                else if (xml_strcmp(attr[i], "capacity") == 0) {
+                else if (xml_strequal(attr[i], "capacity")) {
                     btype->capacity = xml_int(attr[i + 1]);
                 }
-                else if (xml_strcmp(attr[i], "maxcapacity") == 0) {
+                else if (xml_strequal(attr[i], "maxcapacity")) {
                     btype->maxcapacity = xml_int(attr[i + 1]);
                 }
-                else if (xml_strcmp(attr[i], "magresbonus") == 0) {
+                else if (xml_strequal(attr[i], "magresbonus")) {
                     btype->magresbonus = xml_int(attr[i + 1]);
                 }
-                else if (xml_strcmp(attr[i], "fumblebonus") == 0) {
+                else if (xml_strequal(attr[i], "fumblebonus")) {
                     btype->fumblebonus = xml_int(attr[i + 1]);
                 }
-                else if (xml_strcmp(attr[i], "taxes") == 0) {
+                else if (xml_strequal(attr[i], "taxes")) {
                     btype->taxes = xml_int(attr[i + 1]);
                 }
-                else if (xml_strcmp(attr[i], "auraregen") == 0) {
+                else if (xml_strequal(attr[i], "auraregen")) {
                     btype->auraregen = xml_int(attr[i + 1]);
                 }
-                else if (xml_strcmp(attr[i], "magres") == 0) {
+                else if (xml_strequal(attr[i], "magres")) {
                     /* magres is specified in percent! */
                     btype->magres = frac_make(xml_int(attr[i + 1]), 100);
                 }
                 else if (!handle_flag(&flags, attr + i, flag_names)) {
                     /* we already handled the name earlier */
-                    if (xml_strcmp(attr[i], "name") != 0) {
+                    if (!xml_strequal(attr[i], "name")) {
                         handle_bad_input(pi, el, attr[i]);
                     }
                 }
@@ -1235,20 +1259,20 @@ static void start_buildings(parseinfo *pi, const XML_Char *el, const XML_Char **
     else {
         building_type *btype = (building_type *)pi->object;
         assert(btype);
-        if (xml_strcmp(el, "modifier") == 0) {
+        if (xml_strequal(el, "modifier")) {
             handle_modifier(pi, el, attr);
         }
-        else if (xml_strcmp(el, "requirement") == 0) {
+        else if (xml_strequal(el, "requirement")) {
             assert(stage);
             assert(stage->construction);
             handle_requirement(pi, el, attr);
         }
-        else if (xml_strcmp(el, "construction") == 0) {
+        else if (xml_strequal(el, "construction")) {
             assert(stage == NULL);
             stage = calloc(1, sizeof(building_stage));
             stage->construction = parse_construction(pi, el, attr);
         }
-        else if (xml_strcmp(el, "maintenance") == 0) {
+        else if (xml_strequal(el, "maintenance")) {
             assert(!btype->maintenance);
             handle_maintenance(pi, el, attr);
         }
@@ -1262,31 +1286,31 @@ static void XMLCALL handle_start(void *data, const XML_Char *el, const XML_Char
     parseinfo *pi = (parseinfo *)data;
     if (pi->depth == 0) {
         pi->type = EXP_UNKNOWN;
-        if (xml_strcmp(el, "eressea") != 0) {
+        if (!xml_strequal(el, "eressea")) {
             handle_bad_input(pi, el, NULL);
         }
     }
     else if (pi->depth == 1) {
-        if (xml_strcmp(el, "resources") == 0) {
+        if (xml_strequal(el, "resources")) {
             pi->type = EXP_RESOURCES;
         }
-        else if (xml_strcmp(el, "buildings") == 0) {
+        else if (xml_strequal(el, "buildings")) {
             pi->type = EXP_BUILDINGS;
         }
-        else if (xml_strcmp(el, "ships") == 0) {
+        else if (xml_strequal(el, "ships")) {
             pi->type = EXP_SHIPS;
         }
-        else if (xml_strcmp(el, "messages") == 0) {
+        else if (xml_strequal(el, "messages")) {
             pi->type = EXP_MESSAGES;
         }
-        else if (xml_strcmp(el, "spells") == 0) {
+        else if (xml_strequal(el, "spells")) {
             pi->type = EXP_SPELLS;
         }
-        else if (xml_strcmp(el, "spellbook") == 0) {
+        else if (xml_strequal(el, "spellbook")) {
             pi->type = EXP_SPELLBOOKS;
             start_spellbooks(pi, el, attr);
         }
-        else if (xml_strcmp(el, "races") == 0) {
+        else if (xml_strequal(el, "races")) {
             pi->type = EXP_RACES;
         }
         else {
@@ -1332,10 +1356,10 @@ static void XMLCALL handle_start(void *data, const XML_Char *el, const XML_Char
 }
 
 static void end_spells(parseinfo *pi, const XML_Char *el) {
-    if (xml_strcmp(el, "spells") == 0) {
+    if (xml_strequal(el, "spells")) {
         pi->type = EXP_UNKNOWN;
     }
-    else if (xml_strcmp(el, "spell") == 0) {
+    else if (xml_strequal(el, "spell")) {
         spell *sp = (spell *)pi->object;
         if (ncomponents > 0) {
             sp->components = calloc(sizeof(spell_component), ncomponents + 1);
@@ -1350,10 +1374,10 @@ static void end_weapon(parseinfo *pi, const XML_Char *el) {
     resource_type *rtype = (resource_type *)pi->object;
     assert(rtype && rtype->wtype);
 
-    if (xml_strcmp(el, "weapon") == 0) {
+    if (xml_strequal(el, "weapon")) {
         pi->type = EXP_RESOURCES;
     }
-    else if (xml_strcmp(el, "modifier") == 0) {
+    else if (xml_strequal(el, "modifier")) {
         if (nwmods > 0) {
             weapon_type *wtype = rtype->wtype;
             wtype->modifiers = calloc(sizeof(weapon_mod), nwmods + 1);
@@ -1365,14 +1389,14 @@ static void end_weapon(parseinfo *pi, const XML_Char *el) {
 
 static void end_resources(parseinfo *pi, const XML_Char *el) {
     resource_type *rtype = (resource_type *)pi->object;
-    if (xml_strcmp(el, "resource") == 0) {
+    if (xml_strequal(el, "resource")) {
         if (nrmods > 0) {
             rtype->modifiers = calloc(sizeof(resource_mod), nrmods + 1);
             memcpy(rtype->modifiers, rmods, sizeof(resource_mod) * nrmods);
             nrmods = 0;
         }
     }
-    else if (xml_strcmp(el, "construction") == 0) {
+    else if (xml_strequal(el, "construction")) {
         if (nreqs > 0) {
             construction *con = rtype->itype->construction;
             con->materials = calloc(sizeof(requirement), nreqs + 1);
@@ -1380,14 +1404,14 @@ static void end_resources(parseinfo *pi, const XML_Char *el) {
             nreqs = 0;
         }
     }
-    else if (xml_strcmp(el, "resources") == 0) {
+    else if (xml_strequal(el, "resources")) {
         pi->type = EXP_UNKNOWN;
     }
 }
 
 static void end_races(parseinfo *pi, const XML_Char *el) {
     race *rc = (race *)pi->object;
-    if (xml_strcmp(el, "race") == 0) {
+    if (xml_strequal(el, "race")) {
         assert(rc);
         rc->attack[nattacks].type = AT_NONE;
         nattacks = 0;
@@ -1400,14 +1424,14 @@ static void end_races(parseinfo *pi, const XML_Char *el) {
         nfamiliars = 0;
         pi->object = NULL;
     }
-    else if (xml_strcmp(el, "races") == 0) {
+    else if (xml_strequal(el, "races")) {
         pi->type = EXP_UNKNOWN;
     }
 }
 
 static void end_ships(parseinfo *pi, const XML_Char *el) {
     ship_type *stype = (ship_type *)pi->object;
-    if (xml_strcmp(el, "construction") == 0) {
+    if (xml_strequal(el, "construction")) {
         assert(stype);
         assert(stype->construction);
         if (nreqs > 0) {
@@ -1417,7 +1441,7 @@ static void end_ships(parseinfo *pi, const XML_Char *el) {
             nreqs = 0;
         }
     }
-    else if (xml_strcmp(el, "ship") == 0) {
+    else if (xml_strequal(el, "ship")) {
         if (ncoasts > 0) {
             stype->coasts = calloc(sizeof(const terrain_type *), ncoasts + 1);
             memcpy(stype->coasts, coasts, sizeof(const terrain_type *) * ncoasts);
@@ -1425,7 +1449,7 @@ static void end_ships(parseinfo *pi, const XML_Char *el) {
         }
         pi->object = NULL;
     }
-    else if (xml_strcmp(el, "ships") == 0) {
+    else if (xml_strequal(el, "ships")) {
         pi->type = EXP_UNKNOWN;
     }
 }
@@ -1435,7 +1459,7 @@ static void end_buildings(parseinfo *pi, const XML_Char *el) {
     static building_stage **stage_ptr;
 
     building_type *btype = (building_type *)pi->object;
-    if (xml_strcmp(el, "construction") == 0) {
+    if (xml_strequal(el, "construction")) {
         assert(btype);
         if (stage) {
             if (nreqs > 0) {
@@ -1454,7 +1478,7 @@ static void end_buildings(parseinfo *pi, const XML_Char *el) {
             stage = NULL;
         }
     }
-    else if (xml_strcmp(el, "building") == 0) {
+    else if (xml_strequal(el, "building")) {
         stage_ptr = NULL;
         if (nupkeep > 0) {
             btype->maintenance = calloc(sizeof(maintenance), nupkeep + 1);
@@ -1468,7 +1492,7 @@ static void end_buildings(parseinfo *pi, const XML_Char *el) {
         }
         pi->object = NULL;
     }
-    else if (xml_strcmp(el, "buildings") == 0) {
+    else if (xml_strequal(el, "buildings")) {
         pi->type = EXP_UNKNOWN;
     }
 }
diff --git a/src/util/pofile.c b/src/util/pofile.c
index 63ee6dcc8..8de04364d 100644
--- a/src/util/pofile.c
+++ b/src/util/pofile.c
@@ -66,6 +66,7 @@ int pofile_read(const char *filename, int (*callback)(const char *msgid, const c
 
     if (!F) {
         log_error("could not open %s", filename);
+        return -1;
     }
 
     msgctxt[0] = 0;
@@ -106,10 +107,10 @@ int pofile_read(const char *filename, int (*callback)(const char *msgid, const c
             line = read_line(F);
         }
     }
-    if (ferror(F)) {
-        log_error("read error in %s:%d.", filename, po_lineno);
-        return -1;
-    }
+    err = ferror(F);
     fclose(F);
+    if (err) {
+        log_error("read error %d in %s:%d.", err, filename, po_lineno);
+    }
     return err;
 }

From 754456523745fd6e69b6341fdb813db339a72b6f Mon Sep 17 00:00:00 2001
From: Enno Rehling <enno@kn-bremen.de>
Date: Tue, 4 Sep 2018 15:28:05 +0200
Subject: [PATCH 62/62] start work on 3.18 release

---
 src/kernel/version.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/kernel/version.c b/src/kernel/version.c
index 994291ec2..0cfad7b73 100644
--- a/src/kernel/version.c
+++ b/src/kernel/version.c
@@ -7,7 +7,7 @@
 
 #ifndef ERESSEA_VERSION
 /* the version number, if it was not passed to make with -D */
-#define ERESSEA_VERSION "3.17.0"
+#define ERESSEA_VERSION "3.18.0"
 #endif
 
 const char *eressea_version(void) {