]> granicus.if.org Git - nethack/commitdiff
Chronicle of major events, and livelog
authorPasi Kallinen <paxed@alt.org>
Wed, 9 Feb 2022 20:36:19 +0000 (22:36 +0200)
committerPasi Kallinen <paxed@alt.org>
Wed, 9 Feb 2022 20:49:25 +0000 (22:49 +0200)
Log game events, such as entering a new dungeon level, breaking
a conduct, or killing a unique monster, in a new "Major events"
chronicle. The entries record the turn when the event happened.
The log can be viewed with #chronicle -command, and the entries
also show up in the end-of-game dump, if that is available.

This feature is on by default, but can be disabled by
defining NO_CHRONICLE compile-time option.

This also contains "live logging", writing the events as they
happen into a single livelog-file. This is mostly useful for
public servers. The livelog is off by default, and must be
compiled in with LIVELOG, and then turned on in sysconf.

Mostly this a version of livelogging from the Hardfought server,
with some changes.

43 files changed:
doc/fixes3-7-0.txt
include/config.h
include/decl.h
include/extern.h
include/global.h
include/patchlevel.h
include/sys.h
src/attrib.c
src/cmd.c
src/decl.c
src/do.c
src/do_name.c
src/dothrow.c
src/eat.c
src/end.c
src/engrave.c
src/files.c
src/fountain.c
src/hack.c
src/insight.c
src/minion.c
src/mon.c
src/pickup.c
src/pline.c
src/polyself.c
src/potion.c
src/pray.c
src/priest.c
src/read.c
src/restore.c
src/save.c
src/shk.c
src/spell.c
src/sys.c
src/uhitm.c
src/write.c
src/zap.c
sys/unix/Makefile.top
sys/unix/NetHack.xcodeproj/project.pbxproj
sys/unix/hints/include/cross-post.370
sys/unix/hints/linux.370
sys/unix/hints/macOS.370
sys/unix/sysconf

index 3c1026ccc54909bde461abe52fa1346aa62e29ea..b70838afbd0eb109080fb1524519154d30a8f9ce 100644 (file)
@@ -1391,6 +1391,7 @@ cancellation explodes most magical traps
 reading a blessed scroll of light has a chance to improve bless/curse state
        of wielded Sunsword or worn gold dragon scales/mail similar to dipping
        those into holy water; cursed scroll has chance to worsen the state
+added a chronicle of major events, and optional live logging of those
 
 
 Platform- and/or Interface-Specific New Features
index 1e7bc353afa6d7d153ee1bc4fe8c3c1d1f3caa0f..29ff94b621b2d9121c08363b0726ddbbfa161d81 100644 (file)
 /*
  * Section 2:   Some global parameters and filenames.
  *
- *              LOGFILE, XLOGFILE, NEWS and PANICLOG refer to files in
- *              the playground directory.  Commenting out LOGFILE, XLOGFILE,
- *              NEWS or PANICLOG removes that feature from the game.
+ *              LOGFILE, XLOGFILE, LIVELOGFILE, NEWS and PANICLOG refer to
+ *              files in the playground directory.  Commenting out LOGFILE,
+ *              XLOGFILE, NEWS or PANICLOG removes that feature from the game.
  *
  *              Building with debugging features enabled is now unconditional;
  *              the old WIZARD setting for that has been eliminated.
@@ -598,6 +598,21 @@ typedef unsigned char uchar;
    whole thing, then type a new end for the text. */
 /* #define EDIT_GETLIN */
 
+#ifndef NO_CHRONICLE
+/* CHRONICLE - enable #chronicle command, a log of major game events.
+   The logged messages will also appear in DUMPLOG. */
+#define CHRONICLE
+#ifdef CHRONICLE
+/* LIVELOG - log CHRONICLE events into LIVELOGFILE as they happen. */
+/* #define LIVELOG */
+#ifdef LIVELOG
+#define LIVELOGFILE "livelog" /* in-game events recorded, live */
+#endif /* LIVELOG */
+#endif /* CHRONICLE */
+#else
+#undef LIVELOG
+#endif /* NO_CHRONICLE */
+
 /* #define DUMPLOG */  /* End-of-game dump logs */
 #ifdef DUMPLOG
 
index dff6387e76d10e53cff584e4cf3855f2937f42a0..934cc90dd18b6f6a12ba663c99406dda405426e0 100644 (file)
@@ -177,6 +177,14 @@ struct kinfo {
     char name[BUFSZ]; /* actual killer name */
 };
 
+/* game events log */
+struct gamelog_line {
+    long turn; /* turn when this happened */
+    long flags; /* LL_foo flags */
+    char *text;
+    struct gamelog_line *next;
+};
+
 enum movemodes {
     MV_ANY = -1,
     MV_WALK,
@@ -1082,6 +1090,7 @@ struct instance_globals {
     /* work buffer for You(), &c and verbalize() */
     char *you_buf;
     int you_buf_siz;
+    struct gamelog_line *gamelog;
 
     /* polyself.c */
     int sex_change_ok; /* controls whether taking on new form or becoming new
index a2bf6ca7ea949b9c030e61a019fb5b749c0e1d10..4f565a1e64924705822f4a3b847dacdfe5b8bc8a 100644 (file)
@@ -260,6 +260,7 @@ extern void rhack(char *);
 extern int doextlist(void);
 extern int extcmd_via_menu(void);
 extern int enter_explore_mode(void);
+extern int do_gamelog(void);
 extern boolean bind_key(uchar, const char *);
 extern void dokeylist(void);
 extern int xytod(schar, schar);
@@ -879,6 +880,7 @@ extern void reveal_paths(void);
 extern boolean read_tribute(const char *, const char *, int, char *, int,
                             unsigned);
 extern boolean Death_quote(char *, int);
+extern void livelog_add(unsigned int ll_type, const char *);
 
 /* ### fountain.c ### */
 
@@ -2046,6 +2048,8 @@ extern void You_see(const char *, ...) PRINTF_F(1, 2);
 extern void pline_The(const char *, ...) PRINTF_F(1, 2);
 extern void There(const char *, ...) PRINTF_F(1, 2);
 extern void verbalize(const char *, ...) PRINTF_F(1, 2);
+extern void gamelog_add(unsigned int, long, const char *);
+extern void livelog_printf(unsigned int, const char *, ...) PRINTF_F(2, 3);
 extern void raw_printf(const char *, ...) PRINTF_F(1, 2);
 extern void impossible(const char *, ...) PRINTF_F(1, 2);
 extern void config_error_add(const char *, ...) PRINTF_F(1, 2);
index 7ee316945064c0bc381a1622ac2eb07cd1effb80..1aea2e939fe0d1c9fbf5e64bd16733c481e61275 100644 (file)
@@ -463,4 +463,21 @@ extern struct nomakedefs_s nomakedefs;
 #define unctrl(c) ((c) <= C('z') ? (0x60 | (c)) : (c))
 #define unmeta(c) (0x7f & (c))
 
+/* Game log message type flags */
+#define LL_NONE       0x0000 /* No message is livelogged */
+#define LL_WISH       0x0001 /* Report stuff people type at the wish prompt */
+#define LL_ACHIEVE    0x0002 /* Achievements bitfield + invocation, planes */
+#define LL_UMONST     0x0004 /* Kill, Bribe or otherwise dispatch unique monsters */
+#define LL_DIVINEGIFT 0x0008 /* Sacrifice gifts, crowning */
+#define LL_LIFESAVE   0x0010 /* Use up amulet of lifesaving */
+#define LL_CONDUCT    0x0020 /* Break conduct - not reported early-game */
+#define LL_ARTIFACT   0x0040 /* Excalibur, Sting, Orcrist, plus sac gifts and artwishes */
+#define LL_GENOCIDE   0x0080 /* Logging of genocides */
+#define LL_KILLEDPET  0x0100 /* Killed a tame monster */
+#define LL_ALIGNMENT  0x0200 /* changed alignment temporarily or permanently */
+#define LL_DUMP_ASC   0x0400 /* Log URL for dumplog if ascended */
+#define LL_DUMP_ALL   0x0800 /* Log dumplog url for all games */
+#define LL_MINORAC    0x1000 /* Log 'minor' achievements - can be spammy */
+#define LL_DEBUG      0x8000 /* For debugging messages and other spam */
+
 #endif /* GLOBAL_H */
index 7d87ab226000d2d670070e5245b365ea63cb276c..11f6d9d288a9ce898082d3dd720c0d90d89a4665 100644 (file)
@@ -17,7 +17,7 @@
  * Incrementing EDITLEVEL can be used to force invalidation of old bones
  * and save files.
  */
-#define EDITLEVEL 47
+#define EDITLEVEL 48
 
 /*
  * Development status possibilities.
index ef794e747cf352ce347f06745dd9b3453e181683..78cf47eee702423bcf5529e8ea5a94693433e158 100644 (file)
@@ -28,6 +28,7 @@ struct sysopt {
     int check_save_uid; /* restoring savefile checks UID? */
     int check_plname; /* use plname for checking wizards/explorers/shellers */
     int bones_pools;
+    unsigned int livelog; /* LL_foo events to livelog */
 
     /* record file */
     int persmax;
index bb326ac52918c55e15a32b1cf4fc5c5dd79deb1e..2a2dbf6e9199eb8ad76b733c253a6a599973b340 100644 (file)
@@ -1149,6 +1149,8 @@ uchangealign(int newalign,
     g.context.botl = TRUE; /* status line needs updating */
     if (reason == 0) {
         /* conversion via altar */
+        livelog_printf(LL_ALIGNMENT, "permanently converted to %s",
+                       aligns[1 - newalign].adj);
         u.ualignbase[A_CURRENT] = (aligntyp) newalign;
         /* worn helm of opposite alignment might block change */
         if (!uarmh || uarmh->otyp != HELM_OF_OPPOSITE_ALIGNMENT)
@@ -1157,6 +1159,11 @@ uchangealign(int newalign,
             (u.ualign.type != oldalign) ? "sudden " : "");
     } else {
         /* putting on or taking off a helm of opposite alignment */
+        if (reason == 1) {
+            /* don't livelog taking it back off */
+            livelog_printf(LL_ALIGNMENT, "used a helm to turn %s",
+                           aligns[1 - newalign].adj);
+        }
         u.ualign.type = (aligntyp) newalign;
         if (reason == 1)
             Your("mind oscillates %s.", Hallucination ? "wildly" : "briefly");
index 617084ee564037668876c39fc2b2ba08258688cf..bcafdd2bd25839ed97c2a737aed8512dfb83d242 100644 (file)
--- a/src/cmd.c
+++ b/src/cmd.c
@@ -850,6 +850,36 @@ enter_explore_mode(void)
     return ECMD_OK;
 }
 
+int
+do_gamelog(void)
+{
+#ifdef CHRONICLE
+    struct gamelog_line *tmp = g.gamelog;
+    winid win;
+    char buf[BUFSZ];
+
+    if (!tmp) {
+        pline("No chronicled events.");
+        return ECMD_OK;
+    }
+
+    win = create_nhwindow(NHW_TEXT);
+    putstr(win, 0, "Major events:");
+    putstr(win, 0, "");
+    putstr(win, 0, " Turn");
+    while (tmp) {
+        Sprintf(buf, "%5li: %s", tmp->turn, tmp->text);
+        putstr(win, 0, buf);
+        tmp = tmp->next;
+    }
+    display_nhwindow(win, TRUE);
+    destroy_nhwindow(win);
+#else
+    pline("Chronicle was turned off during compile-time.");
+#endif /* !CHRONICLE */
+    return ECMD_OK;
+}
+
 /* #wizwish command - wish for something */
 static int
 wiz_wish(void) /* Unlimited wishes for debug mode by Paul Polderman */
@@ -2138,6 +2168,8 @@ struct ext_func_tab extcmdlist[] = {
               docast, IFBURIED, NULL },
     { M('c'), "chat", "talk to someone",
               dotalk, IFBURIED | AUTOCOMPLETE, NULL },
+    { '\0',   "chronicle", "show journal of major events",
+              do_gamelog, IFBURIED | GENERALCMD, NULL },
     { 'c',    "close", "close a door",
               doclose, 0, NULL },
     { M('C'), "conduct", "list voluntary challenges you have maintained",
index 56514cd453573953757634a48fd121a833095507..180b9f7a033f36f77c0c45ed4da042ae798cc197 100644 (file)
@@ -550,6 +550,7 @@ const struct instance_globals g_init = {
 #endif
     (char *) 0, /* you_buf */
     0, /* you_buf_siz */
+    NULL, /* gamelog */
 
     /* polyself.c */
     0, /* sex_change_ok */
index 3f881e3b724a21603fb4d98c22f6c45b7b473e1f..9588fd91fd2e58c8e9013c9b6ee21e9a61147175 100644 (file)
--- a/src/do.c
+++ b/src/do.c
@@ -294,7 +294,10 @@ doaltarobj(struct obj *obj)
 
     if (obj->oclass != COIN_CLASS) {
         /* KMH, conduct */
-        u.uconduct.gnostic++;
+        if (!u.uconduct.gnostic++)
+            livelog_printf(LL_CONDUCT,
+                           "eschewed atheism, by dropping %s on an altar",
+                           doname(obj));
     } else {
         /* coins don't have bless/curse status */
         obj->blessed = obj->cursed = 0;
@@ -1502,6 +1505,8 @@ goto_level(
         }
         mklev();
         new = TRUE; /* made the level */
+        livelog_printf(LL_DEBUG, "entered new level %d, %s",
+                       dunlev(&u.uz), g.dungeons[u.uz.dnum].dname);
 
         familiar = bones_include_name(g.plname);
     } else {
index bdc2a8c6d7ff707905da8d7f0dd63579d381ab19..392a7993add7e41c84910faa5d7aa59b6ec0bc0f 100644 (file)
@@ -1343,7 +1343,14 @@ oname(struct obj *obj, const char *name)
             alter_cost(obj, 0L);
         if (g.via_naming) {
             /* violate illiteracy conduct since successfully wrote arti-name */
-            u.uconduct.literate++;
+            if (!u.uconduct.literate++)
+                livelog_printf(LL_CONDUCT | LL_ARTIFACT,
+                               "became literate by naming %s",
+                               bare_artifactname(obj));
+            else
+                livelog_printf(LL_ARTIFACT,
+                               "chose %s to be named \"%s\"",
+                               ansimpleoname(obj), bare_artifactname(obj));
         }
     }
     if (carried(obj))
index c9d07789b721c954825acc5e7701d35aa99a2343..9225fcaa0763fd20c7c9764cef7ec6a18c2f673f 100644 (file)
@@ -1880,7 +1880,8 @@ thitmonst(
 
             /* attack hits mon */
             if (hmode == HMON_APPLIED)
-                u.uconduct.weaphit++;
+                if (!u.uconduct.weaphit++)
+                    livelog_printf(LL_CONDUCT, "hit with a wielded weapon for the first time");
             if (hmon(mon, obj, hmode, dieroll)) { /* mon still alive */
                 if (mon->wormno)
                     cutworm(mon, g.bhitpos.x, g.bhitpos.y, chopper);
index ce264a2ab0f043964fde58e49df237bade28d767..8e17c4847fdc56891429c69371bc58af86341f4d 100644 (file)
--- a/src/eat.c
+++ b/src/eat.c
@@ -459,11 +459,27 @@ done_eating(boolean message)
 void
 eating_conducts(struct permonst *pd)
 {
-    u.uconduct.food++;
-    if (!vegan(pd))
-        u.uconduct.unvegan++;
-    if (!vegetarian(pd))
+    int ll_conduct = 0;
+
+    if (!u.uconduct.food++) {
+        livelog_printf(LL_CONDUCT, "ate for the first time - %s",
+                       pd->pmnames[NEUTRAL]);
+        ll_conduct++;
+    }
+    if (!vegan(pd)) {
+        if (!u.uconduct.unvegan++ && !ll_conduct) {
+            livelog_printf(LL_CONDUCT,
+                           "consumed animal products (%s) for the first time",
+                           pd->pmnames[NEUTRAL]);
+            ll_conduct++;
+        }
+    }
+    if (!vegetarian(pd)) {
+        if (!u.uconduct.unvegetarian && !ll_conduct)
+            livelog_printf(LL_CONDUCT, "tasted meat (%s) for the first time",
+                           pd->pmnames[NEUTRAL]);
         violated_vegetarian();
+    }
 }
 
 /* handle side-effects of mind flayer's tentacle attack */
@@ -1017,7 +1033,10 @@ cpostfx(int pm)
         if (g.youmonst.data->mlet != S_MIMIC && !Unchanging) {
             char buf[BUFSZ];
 
-            u.uconduct.polyselfs++; /* you're changing form */
+            if (!u.uconduct.polyselfs++) /* you're changing form */
+                livelog_printf(LL_CONDUCT,
+                               "changed form for the first time by mimicking %s",
+                               Hallucination ? "an orange" : "a pile of gold");
             You_cant("resist the temptation to mimic %s.",
                      Hallucination ? "an orange" : "a pile of gold");
             /* A pile of gold can't ride. */
@@ -1428,7 +1447,9 @@ consume_tin(const char *mesg)
          * Same order as with non-spinach above:
          * conduct update, side-effects, shop handling, and nutrition.
          */
-        u.uconduct.food++; /* don't need vegetarian checks for spinach */
+        /* don't need vegetarian checks for spinach */
+        if (!u.uconduct.food++)
+            livelog_printf(LL_CONDUCT, "ate for the first time (spinach)");
         if (!tin->cursed)
             pline("This makes you feel like %s!",
                   /* "Swee'pea" is a character from the Popeye cartoons */
@@ -1614,6 +1635,7 @@ eatcorpse(struct obj *otmp)
 {
     int retcode = 0, tp = 0, mnum = otmp->corpsenm;
     long rotted = 0L;
+    int ll_conduct = 0;
     boolean stoneable = (flesh_petrifies(&mons[mnum]) && !Stone_resistance
                          && !poly_when_stoned(g.youmonst.data)),
             slimeable = (mnum == PM_GREEN_SLIME && !Slimed && !Unchanging
@@ -1622,10 +1644,18 @@ eatcorpse(struct obj *otmp)
 
     /* KMH, conduct */
     if (!vegan(&mons[mnum]))
-        u.uconduct.unvegan++;
-    if (!vegetarian(&mons[mnum]))
+        if (!u.uconduct.unvegan++) {
+            livelog_printf(LL_CONDUCT,
+                           "consumed animal products for the first time, by eating %s",
+                           an(food_xname(otmp, FALSE)));
+            ll_conduct++;
+        }
+    if (!vegetarian(&mons[mnum])) {
+        if (!u.uconduct.unvegetarian && !ll_conduct)
+            livelog_printf(LL_CONDUCT, "tasted meat for the first time, by eating %s",
+                           an(food_xname(otmp, FALSE)));
         violated_vegetarian();
-
+    }
     if (!nonrotting_corpse(mnum)) {
         long age = peek_at_iced_corpse_age(otmp);
 
@@ -2233,7 +2263,8 @@ fpostfx(struct obj *otmp)
     case FORTUNE_COOKIE:
         outrumor(bcsign(otmp), BY_COOKIE);
         if (!Blind)
-            u.uconduct.literate++;
+            if (!u.uconduct.literate++)
+                livelog_printf(LL_CONDUCT, "became literate by reading the fortune inside a cookie");
         break;
     case LUMP_OF_ROYAL_JELLY:
         /* This stuff seems to be VERY healthy! */
@@ -2489,6 +2520,7 @@ doeat(void)
     int basenutrit; /* nutrition of full item */
     boolean dont_start = FALSE, nodelicious = FALSE,
             already_partly_eaten;
+    int ll_conduct = 0;
 
     if (Strangled) {
         pline("If you can't breathe air, how can you consume solids?");
@@ -2614,14 +2646,27 @@ doeat(void)
         g.context.victual.nmod = basenutrit;
         g.context.victual.eating = TRUE; /* needed for lesshungry() */
 
+        if (!u.uconduct.food++) {
+            ll_conduct++;
+            livelog_printf(LL_CONDUCT, "ate for the first time (%s)",
+                           food_xname(otmp, FALSE));
+        }
         material = objects[otmp->otyp].oc_material;
         if (material == LEATHER || material == BONE
             || material == DRAGON_HIDE) {
-            u.uconduct.unvegan++;
+            if (!u.uconduct.unvegan++ && !ll_conduct) {
+                livelog_printf(LL_CONDUCT, "consumed animal products for the first time, by eating %s",
+                               an(food_xname(otmp, FALSE)));
+                ll_conduct++;
+            }
+            if (!u.uconduct.unvegetarian && !ll_conduct)
+                livelog_printf(LL_CONDUCT, "tasted meat for the first time, by eating %s",
+                               an(food_xname(otmp, FALSE)));
             violated_vegetarian();
         } else if (material == WAX)
-            u.uconduct.unvegan++;
-        u.uconduct.food++;
+            if (!u.uconduct.unvegan++ && !ll_conduct)
+                livelog_printf(LL_CONDUCT, "consumed animal products for the first time, by eating %s",
+                               an(food_xname(otmp, FALSE)));
 
         if (otmp->cursed) {
             (void) rottenfood(otmp);
@@ -2684,7 +2729,10 @@ doeat(void)
     }
 
     /* KMH, conduct */
-    u.uconduct.food++;
+    if (!u.uconduct.food++) {
+        livelog_printf(LL_CONDUCT, "ate for the first time - %s", food_xname(otmp, FALSE));
+        ll_conduct++;
+    }
 
     already_partly_eaten = otmp->oeaten ? TRUE : FALSE;
     g.context.victual.piece = otmp = touchfood(otmp);
@@ -2714,8 +2762,16 @@ doeat(void)
          */
         switch (objects[otmp->otyp].oc_material) {
         case FLESH:
-            u.uconduct.unvegan++;
+            if (!u.uconduct.unvegan++ && !ll_conduct) {
+                livelog_printf(LL_CONDUCT, "consumed animal products for the first time, by eating %s",
+                               an(food_xname(otmp, FALSE)));
+                ll_conduct++;
+            }
             if (otmp->otyp != EGG) {
+                if (!u.uconduct.unvegetarian && !ll_conduct)
+                    livelog_printf(LL_CONDUCT, "tasted meat for the first time, by eating %s",
+                                   an(food_xname(otmp, FALSE)));
+
                 violated_vegetarian();
             }
             break;
@@ -2724,7 +2780,9 @@ doeat(void)
             if (otmp->otyp == PANCAKE || otmp->otyp == FORTUNE_COOKIE /*eggs*/
                 || otmp->otyp == CREAM_PIE || otmp->otyp == CANDY_BAR /*milk*/
                 || otmp->otyp == LUMP_OF_ROYAL_JELLY)
-                u.uconduct.unvegan++;
+                if (!u.uconduct.unvegan++ && !ll_conduct)
+                    livelog_printf(LL_CONDUCT, "consumed animal products (%s) for the first time",
+                                   food_xname(otmp, FALSE));
             break;
         }
 
index f842ae038185460f402ba2be3ef18b2274389887..4ed8d70108f11ac8b08c124caa8c98e8ec30ed7f 100644 (file)
--- a/src/end.c
+++ b/src/end.c
@@ -793,6 +793,8 @@ dump_everything(int how,
 
     dump_plines();
     putstr(0, 0, "");
+    (void) do_gamelog();
+    putstr(0, 0, "");
     putstr(0, 0, "Inventory:");
     (void) display_inventory((char *) 0, TRUE);
     container_contents(g.invent, TRUE, TRUE, FALSE);
@@ -1215,6 +1217,9 @@ done(int how)
         if (how == GENOCIDED) {
             pline("Unfortunately you are still genocided...");
         } else {
+            char killbuf[BUFSZ];
+            formatkiller(killbuf, BUFSZ, how, FALSE);
+            livelog_printf(LL_LIFESAVE, "averted death (%s)", killbuf);
             survive = TRUE;
         }
     }
index 44ca6b0d37fe980e9feaa7b2d422de0be3a6a7a2..a3b7db770a3b3a639dac33525f6055dd578bfb36 100644 (file)
@@ -1072,7 +1072,8 @@ doengrave(void)
 
     /* A single `x' is the traditional signature of an illiterate person */
     if (len != 1 || (!index(ebuf, 'x') && !index(ebuf, 'X')))
-        u.uconduct.literate++;
+        if (!u.uconduct.literate++)
+            livelog_printf(LL_CONDUCT, "became literate by engraving \"%s\"", ebuf);
 
     /* Mix up engraving if surface or state of mind is unsound.
        Note: this won't add or remove any spaces. */
index 0a82768213cba24066304c65f898fc2fb814340e..55397a61d2637de6be81404898858fa14e577864 100644 (file)
@@ -2578,6 +2578,13 @@ parse_config_line(char *origbuf)
             n = 10;
         }
         sysopt.tt_oname_maxrank = n;
+    } else if (src == set_in_sysconf && match_varname(buf, "LIVELOG", 7)) {
+        n = strtol(bufp,NULL,0);
+        if (n < 0 || n > 0xFFFF) {
+            raw_printf("Illegal value in LIVELOG (must be between 0 and 0xFFFF).");
+            return 0;
+        }
+        sysopt.livelog = n;
 
     /* SYSCF PANICTRACE options */
     } else if (in_sysconf && match_varname(buf, "PANICTRACE_LIBC", 15)) {
@@ -4658,4 +4665,52 @@ Death_quote(char *buf, int bufsz)
 
 /* ----------  END TRIBUTE ----------- */
 
+#if defined LIVELOG
+#define LLOG_SEP '\t' /* livelog field separator */
+
+/* Locks the live log file and writes 'buffer'
+ * IF the ll_type matches sysopt.livelog mask
+ * lltype is included in LL entry for post-process filtering also
+ */
+void
+livelog_add(unsigned int ll_type, const char *str)
+{
+    FILE* livelogfile;
+
+    if (!(ll_type & sysopt.livelog))
+        return;
+    if (lock_file(LIVELOGFILE, SCOREPREFIX, 10)) {
+        if (!(livelogfile = fopen_datafile(LIVELOGFILE, "a", SCOREPREFIX))) {
+            pline("Cannot open live log file!");
+            unlock_file(LIVELOGFILE);
+            return;
+        }
+        fprintf(livelogfile,
+                 "lltype=%d%cname=%s%crole=%s%crace=%s%cgender=%s%c"
+                 "align=%s%cturns=%ld%cstarttime=%ld%ccurtime=%ld%c"
+                 "message=%s\n",
+                 (ll_type & sysopt.livelog), LLOG_SEP,
+                 g.plname, LLOG_SEP,
+                 g.urole.filecode, LLOG_SEP,
+                 g.urace.filecode, LLOG_SEP,
+                 genders[flags.female].filecode, LLOG_SEP,
+                 aligns[1-u.ualign.type].filecode, LLOG_SEP,
+                 g.moves, LLOG_SEP,
+                 (long)ubirthday, LLOG_SEP,
+                 (long)time(NULL),
+                 LLOG_SEP, str);
+        (void) fclose(livelogfile);
+        unlock_file(LIVELOGFILE);
+    }
+}
+#undef LLOG_SEP
+
+#else
+void
+livelog_add(unsigned int ll_type UNUSED, const char *str UNUSED)
+{
+    /* nothing here */
+}
+#endif /* !LIVELOG */
+
 /*files.c*/
index d7e72d81f54d274b351424981cb6c46d2c56e21f..dbc19f887f2406c8d4fd28cd726612326ca3d477 100644 (file)
@@ -392,6 +392,7 @@ dipfountain(register struct obj *obj)
                 obj->spe--;
             obj->oerodeproof = FALSE;
             exercise(A_WIS, FALSE);
+            livelog_printf(LL_ARTIFACT, "was denied Excalibur! The Lady of the Lake has deemed %s unworthy", uhim());
         } else {
             /* The lady of the lake acts! - Eric Backus */
             /* Be *REAL* nice */
@@ -404,6 +405,7 @@ dipfountain(register struct obj *obj)
             obj->oeroded = obj->oeroded2 = 0;
             obj->oerodeproof = TRUE;
             exercise(A_WIS, TRUE);
+            livelog_printf(LL_ARTIFACT, "was given Excalibur");
         }
         update_inventory();
         levl[u.ux][u.uy].typ = ROOM, levl[u.ux][u.uy].flags = 0;
index 420ccd676b6d4b4f258ef0aba6797c91be64fc25..b15811c62c46289a42415e7e122807272275ff36 100644 (file)
@@ -569,7 +569,17 @@ still_chewing(xchar x, xchar y)
     }
 
     /* Okay, you've chewed through something */
-    u.uconduct.food++;
+    if (!u.uconduct.food++)
+        livelog_printf(LL_CONDUCT, "ate for the first time, by chewing through %s",
+                       boulder
+                       ? "a boulder"
+                       : IS_TREE(lev->typ)
+                       ? "a tree"
+                       : IS_ROCK(lev->typ)
+                       ? "rock"
+                       : (lev->typ == IRONBARS)
+                       ? "iron bars"
+                       : "a door");
     u.uhunger += rnd(20);
 
     if (boulder) {
@@ -2071,7 +2081,8 @@ domove_core(void)
                        killed() so we duplicate some of the latter here */
                     int tmp, mndx;
 
-                    u.uconduct.killer++;
+                    if (!u.uconduct.killer++)
+                        livelog_printf(LL_CONDUCT, "killed for the first time");
                     mndx = monsndx(mtmp->data);
                     tmp = experience(mtmp, (int) g.mvitals[mndx].died);
                     more_experienced(tmp, 0);
index 5b789418b0e424b43c0974d42348e48291c7aac1..077ef6b8328358d819d59146b87373ba8c6d2b76 100644 (file)
@@ -44,6 +44,53 @@ static const char You_[] = "You ", are[] = "are ", were[] = "were ",
 static const char have_been[] = "have been ", have_never[] = "have never ",
                   never[] = "never ";
 
+/* for livelogging: */
+struct ll_achieve_msg {
+    unsigned long llflag;
+    const char *msg;
+};
+/* ordered per 'enum achievements' in you.h */
+/* take care to keep them in sync! */
+static struct ll_achieve_msg achieve_msg [] = {
+    { 0, "" }, /* actual achievements are numbered from 1 */
+    { LL_ACHIEVE, "acquired the Bell of Opening" },
+    { LL_ACHIEVE, "entered Gehennom" },
+    { LL_ACHIEVE, "acquired the Candelabrum of Invocation" },
+    { LL_ACHIEVE, "acquired the Book of the Dead" },
+    { LL_ACHIEVE, "performed the invocation" },
+    { LL_ACHIEVE, "acquired The Amulet of Yendor" },
+    { LL_ACHIEVE, "entered the Planes" },
+    { LL_ACHIEVE, "entered the Astral Plane" },
+    { LL_ACHIEVE, "ascended" },
+    { LL_ACHIEVE, "acquired the Mines' End luckstone" },
+    { LL_ACHIEVE, "completed Sokoban" },
+    { LL_ACHIEVE|LL_UMONST, "killed Medusa" },
+     /* these two are not logged */
+    { 0, "hero was always blond, no, blind" },
+    { 0, "hero never wore armor" },
+     /* */
+    { LL_MINORAC, "entered the Gnomish Mines" },
+    { LL_ACHIEVE, "reached Mine Town" }, /* probably minor, but dnh logs it */
+    { LL_MINORAC, "entered a shop" },
+    { LL_MINORAC, "entered a temple" },
+    { LL_ACHIEVE, "consulted the Oracle" }, /* minor, but rare enough */
+    { LL_ACHIEVE, "read a Discworld novel" }, /* ditto */
+    { LL_ACHIEVE, "entered Sokoban" }, /* Keep as major for turn comparison w/completed soko */
+    { LL_ACHIEVE, "entered the Bigroom" },
+    /* The following 8 are for advancing through the ranks
+       messages differ by role so are created on the fly */
+    { LL_MINORAC, "" },
+    { LL_MINORAC, "" },
+    { LL_MINORAC, "" },
+    { LL_MINORAC, "" },
+    { LL_ACHIEVE, "" },
+    { LL_ACHIEVE, "" },
+    { LL_ACHIEVE, "" },
+    { LL_ACHIEVE, "" },
+    { 0, "" } /* keep this one at the end */
+};
+
+
 #define enl_msg(prefix, present, past, suffix, ps) \
     enlght_line(prefix, final ? past : present, suffix, ps)
 #define you_are(attr, ps) enl_msg(You_, are, were, attr, ps)
@@ -2231,7 +2278,15 @@ record_achievement(schar achidx)
         if (abs(u.uachieved[i]) == abs(achidx))
             return; /* already recorded, don't duplicate it */
     u.uachieved[i] = achidx;
-    return;
+
+    if (g.program_state.gameover)
+        return; /* don't livelog achievements recorded at end of game */
+    if (absidx >= ACH_RNK1 && absidx <= ACH_RNK8) {
+        livelog_printf(achieve_msg[absidx].llflag, "attained the rank of %s",
+                       rank_of(rank_to_xlev(absidx - (ACH_RNK1 - 1)),
+                               Role_switch, (achidx < 0) ? TRUE : FALSE));
+    } else
+        livelog_printf(achieve_msg[absidx].llflag, "%s", achieve_msg[absidx].msg);
 }
 
 /* discard a recorded achievement; return True if removed, False otherwise */
index 70e2815655361c503f9782a44b6d0143e0f396d4..1d17386efe09d37902b444ac884cf4f540380e69 100644 (file)
@@ -331,10 +331,14 @@ demon_talk(register struct monst *mtmp)
         if (!Deaf && ((offer = bribe(mtmp)) >= demand)) {
             pline("%s vanishes, laughing about cowardly mortals.",
                   Amonnam(mtmp));
+            livelog_printf(LL_UMONST, "bribed %s with %ld %s for safe passage",
+                           Amonnam(mtmp), offer, currency(offer));
         } else if (offer > 0L
                    && (long) rnd(5 * ACURR(A_CHA)) > (demand - offer)) {
             pline("%s scowls at you menacingly, then vanishes.",
                   Amonnam(mtmp));
+            livelog_printf(LL_UMONST, "bribed %s with %ld %s for safe passage",
+                           Amonnam(mtmp), offer, currency(offer));
         } else {
             pline("%s gets angry...", Amonnam(mtmp));
             mtmp->mpeaceful = 0;
index 52c63a74df89cb840f4f94dfa109e70f3a12ed2f..137cd2ed5c14dcb8fac982685579071fe017ecc6 100644 (file)
--- a/src/mon.c
+++ b/src/mon.c
@@ -32,6 +32,14 @@ static void kill_eggs(struct obj *);
     (Is_rogue_level(&u.uz)            \
      || (g.level.flags.graveyard && is_undead(mdat) && rn2(3)))
 
+/* A specific combination of x_monnam flags for livelogging. The livelog
+ * shouldn't show that you killed a hallucinatory monster and not what it
+ * actually is. */
+#define livelog_mon_nam(mtmp) \
+    x_monnam(mtmp, ARTICLE_THE, (char *) 0,                 \
+             (SUPPRESS_IT | SUPPRESS_HALLUCINATION), FALSE)
+
+
 #if 0
 /* part of the original warning code which was replaced in 3.3.1 */
 const char *warnings[] = {
@@ -2570,6 +2578,30 @@ mondead(register struct monst* mtmp)
 #endif
     if (mtmp->data == &mons[PM_MEDUSA])
         record_achievement(ACH_MEDU);
+    else if (unique_corpstat(mtmp->data)) {
+        switch (g.mvitals[tmp].died) {
+            case 1:
+                livelog_printf(LL_UMONST, "%s %s",
+                               nonliving(mtmp->data) ? "destroyed" : "killed",
+                               livelog_mon_nam(mtmp));
+                break;
+            case 5:
+            case 10:
+            case 50:
+            case 100:
+            case 150:
+            case 200:
+            case 250:
+                livelog_printf(LL_UMONST, "%s %s (%d times)",
+                               nonliving(mtmp->data) ? "destroyed" : "killed",
+                               livelog_mon_nam(mtmp), g.mvitals[tmp].died);
+                break;
+            default:
+                /* don't spam the log every time */
+                break;
+        }
+    }
+
     if (glyph_is_invisible(levl[mtmp->mx][mtmp->my].glyph))
         unmap_object(mtmp->mx, mtmp->my);
     m_detach(mtmp, mptr);
@@ -2884,7 +2916,8 @@ xkilled(
 
     mtmp->mhp = 0; /* caller will usually have already done this */
     if (!noconduct) /* KMH, conduct */
-        u.uconduct.killer++;
+        if (!u.uconduct.killer++)
+            livelog_printf(LL_CONDUCT, "killed for the first time");
 
     if (!nomsg) {
         boolean namedpet = has_mgivenname(mtmp) && !Hallucination;
@@ -3064,6 +3097,14 @@ xkilled(
             You_hear("the rumble of distant thunder...");
         else
             You_hear("the studio audience applaud!");
+        if (!unique_corpstat(mdat)) {
+            boolean mname = has_mgivenname(mtmp);
+
+            livelog_printf(LL_KILLEDPET, "murdered %s%s%s faithful %s",
+                           mname ? MGIVENNAME(mtmp) : "",
+                           mname ? ", " : "",
+                           uhis(), pmname(mdat, Mgender(mtmp)));
+        }
     } else if (mtmp->mpeaceful)
         adjalign(-5);
 
index 553d7913e0ee024354ef6e603e4d55e9092cd54c..cb4f0f18049cd1c336c8e53d53376d3f17b94a15 100644 (file)
@@ -2404,6 +2404,7 @@ in_container(struct obj *obj)
         if (obj->otyp == BAG_OF_HOLDING) /* one bag of holding into another */
             do_boh_explosion(obj, (obj->where == OBJ_FLOOR));
         obfree(obj, (struct obj *) 0);
+        livelog_printf(LL_ACHIEVE, "just blew up %s bag of holding", uhis());
         /* if carried, shop goods will be flagged 'unpaid' and obfree() will
            handle bill issues, but if on floor, we need to put them on bill
            before deleting them (non-shop items will be flagged 'no_charge') */
index a8cdb7338b9bdf1ade89e8f96642a3607997cfe8..17d472006d5c1c217172acb67d57d7ad2670386e 100644 (file)
@@ -398,6 +398,55 @@ verbalize(const char *line, ...)
     va_end(the_args);
 }
 
+#ifdef CHRONICLE
+
+void
+gamelog_add(unsigned int glflags, long gltime, const char *str)
+{
+    struct gamelog_line *tmp;
+    struct gamelog_line *lst = g.gamelog;
+
+    tmp = (struct gamelog_line *)alloc(sizeof(struct gamelog_line));
+    tmp->turn = gltime;
+    tmp->flags = glflags;
+    tmp->text = strdup(str);
+    tmp->next = NULL;
+    while (lst && lst->next)
+        lst = lst->next;
+    if (!lst)
+        g.gamelog = tmp;
+    else
+        lst->next = tmp;
+}
+
+void
+livelog_printf(unsigned int ll_type, const char *line, ...)
+{
+    char gamelogbuf[BUFSZ * 2];
+    va_list the_args;
+
+    va_start(the_args, line);
+    vsnprintf(gamelogbuf, sizeof gamelogbuf, line, the_args);
+    va_end(the_args);
+
+    gamelog_add(ll_type, g.moves, gamelogbuf);
+    strNsubst(gamelogbuf, "\t", "_", 0);
+    livelog_add(ll_type, gamelogbuf);
+}
+
+#else
+void
+gamelog_add(unsigned int glflags UNUSED, long gltime UNUSED, const char *msg UNUSED)
+{
+    /* nothing here */
+}
+void
+livelog_printf(unsigned int ll_type UNUSED, const char *line UNUSED, ...)
+{
+    /* nothing here */
+}
+#endif /* !CHRONICLE */
+
 static void vraw_printf(const char *, va_list);
 
 void
index e9d75ce71885348e4ad4f4b429a4afe14bf71879..a8983e21a708f33c3c3b6082d55ead33faa9fe5f 100644 (file)
@@ -646,7 +646,10 @@ polymon(int mntmp)
     }
 
     /* KMH, conduct */
-    u.uconduct.polyselfs++;
+    if (!u.uconduct.polyselfs++)
+        livelog_printf(LL_CONDUCT,
+                       "changed form for the first time, becoming %s",
+                       an(pmname(&mons[mntmp], flags.female ? FEMALE : MALE)));
 
     /* exercise used to be at the very end but only Wis was affected
        there since the polymorph was always in effect by then */
index 172cfd10ab72a656dc16833632cd87a2685c9ae3..e36324d26644b3e46315235ca4039e30dab59b8f 100644 (file)
@@ -2262,7 +2262,8 @@ dodip(void)
             short save_otyp = obj->otyp;
 
             /* KMH, conduct */
-            u.uconduct.polypiles++;
+            if (!u.uconduct.polypiles++)
+                livelog_printf(LL_CONDUCT, "polymorphed %s first item", uhis());
 
             obj = poly_obj(obj, STRANGE_OBJECT);
 
index a7158d5fe5581572ec14558a710234fd497804f0..061b9f7b226f3a9544158551822b9ebd249ce2f1 100644 (file)
@@ -791,6 +791,8 @@ gcrownu(void)
     case A_LAWFUL:
         u.uevent.uhand_of_elbereth = 1;
         verbalize("I crown thee...  The Hand of Elbereth!");
+        livelog_printf(LL_DIVINEGIFT,
+                       "was crowned \"The Hand of Elbereth\" by %s", u_gname());
         break;
     case A_NEUTRAL:
         u.uevent.uhand_of_elbereth = 2;
@@ -798,6 +800,8 @@ gcrownu(void)
         already_exists =
             exist_artifact(LONG_SWORD, artiname(ART_VORPAL_BLADE));
         verbalize("Thou shalt be my Envoy of Balance!");
+        livelog_printf(LL_DIVINEGIFT, "became %s Envoy of Balance",
+                       s_suffix(u_gname()));
         break;
     case A_CHAOTIC:
         u.uevent.uhand_of_elbereth = 3;
@@ -808,6 +812,11 @@ gcrownu(void)
                   ((already_exists && !in_hand)
                    || class_gift != STRANGE_OBJECT) ? "take lives"
                   : "steal souls");
+        livelog_printf(LL_DIVINEGIFT, "was chosen to %s for the Glory of %s",
+                       ((already_exists && !in_hand)
+                        || class_gift != STRANGE_OBJECT) ? "take lives"
+                       : "steal souls",
+                       u_gname());
         break;
     }
 
@@ -1369,7 +1378,11 @@ dosacrifice(void)
         struct monst *mtmp;
 
         /* KMH, conduct */
-        u.uconduct.gnostic++;
+        if (!u.uconduct.gnostic++)
+            livelog_printf(LL_CONDUCT,
+                           "rejected atheism by offering %s on an altar of %s",
+                           corpse_xname(otmp, (const char *) 0, CXN_ARTICLE),
+                           a_gname());
 
         /* you're handling this corpse, even if it was killed upon the altar
          */
@@ -1784,6 +1797,10 @@ dosacrifice(void)
                     u.ugifts++;
                     u.ublesscnt = rnz(300 + (50 * nartifacts));
                     exercise(A_WIS, TRUE);
+                    livelog_printf (LL_DIVINEGIFT|LL_ARTIFACT,
+                                    "had %s bestowed upon %s by %s",
+                                    artiname(otmp->oartifact),
+                                    uhim(), align_gname(u.ualign.type));
                     /* make sure we can use this weapon */
                     unrestrict_weapon_skill(weapon_type(otmp));
                     if (!Hallucination && !Blind) {
@@ -1873,7 +1890,13 @@ dopray(void)
     if (ParanoidPray && yn("Are you sure you want to pray?") != 'y')
         return ECMD_OK;
 
-    u.uconduct.gnostic++;
+    if (!u.uconduct.gnostic++)
+        /* breaking conduct should probably occur in can_pray() at
+         * "You begin praying to %s", as demons who find praying repugnant
+         * should not break conduct.  Also we can add more detail to the
+         * livelog message as p_aligntyp will be known.
+         */
+        livelog_printf(LL_CONDUCT, "rejected atheism with a prayer");
 
     /* set up p_type and p_alignment */
     if (!can_pray(TRUE))
@@ -2002,7 +2025,9 @@ doturn(void)
         You("don't know how to turn undead!");
         return ECMD_OK;
     }
-    u.uconduct.gnostic++;
+    if (!u.uconduct.gnostic++)
+        livelog_printf(LL_CONDUCT, "rejected atheism by turning undead");
+
     Gname = halu_gname(u.ualign.type);
 
     /* [What about needing free hands (does #turn involve any gesturing)?] */
index d1c6911c41eaff02e44d446c9432cff7f90dfdc0..c14ce42619ddd2957d18fa1e71e3494ecf246334 100644 (file)
@@ -560,7 +560,10 @@ priest_talk(struct monst *priest)
     boolean strayed = (u.ualign.record < 0);
 
     /* KMH, conduct */
-    u.uconduct.gnostic++;
+    if (!u.uconduct.gnostic++)
+        livelog_printf(LL_CONDUCT,
+                       "rejected atheism by consulting with %s",
+                       mon_nam(priest));
 
     if (priest->mflee || (!priest->ispriest && coaligned && strayed)) {
         pline("%s doesn't want anything to do with you!", Monnam(priest));
index a177923f9d2f270811224e045e67f2d9a75c3975..c944b11f92a5b886e109cde5704263ff77115757 100644 (file)
@@ -363,7 +363,8 @@ doread(void)
             You("break up the cookie and throw away the pieces.");
         outrumor(bcsign(scroll), BY_COOKIE);
         if (!Blind)
-            u.uconduct.literate++;
+            if (!u.uconduct.literate++)
+                livelog_printf(LL_CONDUCT, "became literate by reading a fortune cookie");
         useup(scroll);
         return ECMD_TIME;
     } else if (otyp == T_SHIRT || otyp == ALCHEMY_SMOCK
@@ -388,7 +389,10 @@ doread(void)
                   hawaiian_design(scroll, buf));
             return ECMD_TIME;
         }
-        u.uconduct.literate++;
+        if (!u.uconduct.literate++)
+            livelog_printf(LL_CONDUCT, "became literate by reading %s",
+                           (scroll->otyp == T_SHIRT) ? "a T-shirt" : "an apron");
+
         /* populate 'buf[]' */
         mesg = (otyp == T_SHIRT) ? tshirt_text(scroll, buf)
                                  : apron_text(scroll, buf);
@@ -427,7 +431,10 @@ doread(void)
         pline("%s on the %s.  It reads:  %s.",
               !Blind ? "There is writing" : "You feel lettering",
               simpleonames(scroll), cap_text);
-        u.uconduct.literate++;
+        if (!u.uconduct.literate++)
+            livelog_printf(LL_CONDUCT, "became literate by reading %s",
+                           otyp == DUNCE_CAP ? "a dunce cap" : "a cornuthaum");
+
         /* yet another note: despite the fact that player will recognize
            the object type, don't make it become a discovery for hero */
         if (!objects[otyp].oc_name_known && !objects[otyp].oc_uname)
@@ -470,7 +477,10 @@ doread(void)
               (!((int) scroll->o_id % 3)),
               (((int) scroll->o_id * 7) % 10),
               (flags.verbose || Blind) ? "." : "");
-        u.uconduct.literate++;
+        if (!u.uconduct.literate++)
+            livelog_printf(LL_CONDUCT,
+                                 "became literate by reading a credit card");
+
         return ECMD_TIME;
     } else if (otyp == CAN_OF_GREASE) {
         pline("This %s has no label.", singular(scroll, xname));
@@ -483,7 +493,10 @@ doread(void)
         if (flags.verbose)
             pline("It reads:");
         pline("\"Magic Marker(TM) Red Ink Marker Pen.  Water Soluble.\"");
-        u.uconduct.literate++;
+        if (!u.uconduct.literate++)
+            livelog_printf(LL_CONDUCT,
+                                 "became literate by reading a magic marker");
+
         return ECMD_TIME;
     } else if (scroll->oclass == COIN_CLASS) {
         if (Blind)
@@ -491,7 +504,10 @@ doread(void)
         else if (flags.verbose)
             You("read:");
         pline("\"1 Zorkmid.  857 GUE.  In Frobs We Trust.\"");
-        u.uconduct.literate++;
+        if (!u.uconduct.literate++)
+            livelog_printf(LL_CONDUCT,
+                                 "became literate by reading a coin's engravings");
+
         return ECMD_TIME;
     } else if (scroll->oartifact == ART_ORB_OF_FATE) {
         if (Blind)
@@ -499,7 +515,10 @@ doread(void)
         else
             pline("It is signed:");
         pline("\"Odin.\"");
-        u.uconduct.literate++;
+        if (!u.uconduct.literate++)
+            livelog_printf(LL_CONDUCT,
+                                 "became literate by reading the divine signature of Odin");
+
         return ECMD_TIME;
     } else if (otyp == CANDY_BAR) {
         const char *wrapper = candy_wrapper_text(scroll);
@@ -513,7 +532,10 @@ doread(void)
             return ECMD_OK;
         }
         pline("The wrapper reads: \"%s\".", wrapper);
-        u.uconduct.literate++;
+        if (!u.uconduct.literate++)
+            livelog_printf(LL_CONDUCT,
+                                 "became literate by reading a candy bar wrapper");
+
         return ECMD_TIME;
     } else if (scroll->oclass != SCROLL_CLASS
                && scroll->oclass != SPBOOK_CLASS) {
@@ -560,7 +582,10 @@ doread(void)
     /* Novel conduct is handled in read_tribute so exclude it too */
     if (otyp != SPE_BOOK_OF_THE_DEAD && otyp != SPE_NOVEL
         && otyp != SPE_BLANK_PAPER && otyp != SCR_BLANK_PAPER)
-        u.uconduct.literate++;
+        if (!u.uconduct.literate++)
+            livelog_printf(LL_CONDUCT, "became literate by reading %s",
+                           scroll->oclass == SPBOOK_CLASS ? "a book" :
+                           scroll->oclass == SCROLL_CLASS ? "a scroll" : "something");
 
     if (scroll->oclass == SPBOOK_CLASS) {
         return study_book(scroll) ? ECMD_TIME : ECMD_OK;
@@ -2418,6 +2443,7 @@ static void
 do_class_genocide(void)
 {
     int i, j, immunecnt, gonecnt, goodcnt, class, feel_dead = 0;
+    int ll_done = 0;
     char buf[BUFSZ] = DUMMY;
     boolean gameover = FALSE; /* true iff killed self */
 
@@ -2488,6 +2514,15 @@ do_class_genocide(void)
                     /* This check must be first since player monsters might
                      * have G_GENOD or !G_GENO.
                      */
+                    if (!ll_done++) {
+                        if (!num_genocides())
+                            livelog_printf(LL_CONDUCT | LL_GENOCIDE,
+                                           "performed %s first genocide (class %c)",
+                                           uhis(), def_monsyms[class].sym);
+                        else
+                            livelog_printf(LL_GENOCIDE, "genocided class %c", def_monsyms[class].sym);
+                    }
+
                     g.mvitals[i].mvflags |= (G_GENOD | G_NOCORPSE);
                     kill_genocided_monsters();
                     update_inventory(); /* eggs & tins */
@@ -2665,6 +2700,12 @@ do_genocide(int how)
             which = !type_is_pname(ptr) ? "the " : "";
     }
     if (how & REALLY) {
+        if (!num_genocides())
+            livelog_printf(LL_CONDUCT | LL_GENOCIDE,
+                           "performed %s first genocide (%s)", uhis(), makeplural(buf));
+        else
+            livelog_printf(LL_GENOCIDE, "genocided %s", makeplural(buf));
+
         /* setting no-corpse affects wishing and random tin generation */
         g.mvitals[mndx].mvflags |= (G_GENOD | G_NOCORPSE);
         pline("Wiped out %s%s.", which,
index ae1908dcf1fa496a2e5f44ebfbc957d1cbbd581c..59e61d36b936ce7998fd8bf195e796021fca4339 100644 (file)
@@ -30,6 +30,7 @@ static void ghostfruit(struct obj *);
 static boolean restgamestate(NHFILE *, unsigned int *, unsigned int *);
 static void restlevelstate(unsigned int, unsigned int);
 static int restlevelfile(xchar);
+static void restore_gamelog(NHFILE *);
 static void restore_msghistory(NHFILE *);
 static void reset_oattached_mids(boolean);
 static void rest_levl(NHFILE *, boolean);
@@ -697,6 +698,7 @@ restgamestate(NHFILE* nhfp, unsigned int* stuckid, unsigned int* steedid)
     restnames(nhfp);
     restore_waterlevel(nhfp);
     restore_msghistory(nhfp);
+    restore_gamelog(nhfp);
     /* must come after all mons & objs are restored */
     relink_timers(FALSE);
     relink_light_sources(FALSE);
@@ -1233,6 +1235,29 @@ get_plname_from_file(NHFILE* nhfp, char *plbuf)
     return;
 }
 
+static void
+restore_gamelog(NHFILE* nhfp)
+{
+    int slen = 0;
+    char msg[BUFSZ*2];
+    struct gamelog_line tmp;
+
+    while (1) {
+        if (nhfp->structlevel)
+            mread(nhfp->fd, (genericptr_t)&slen, sizeof(slen));
+        if (slen == -1)
+            break;
+        if (slen > ((BUFSZ*2) - 1))
+            panic("restore_gamelog: msg too big (%d)", slen);
+        if (nhfp->structlevel) {
+            mread(nhfp->fd, (genericptr_t) msg, slen);
+            mread(nhfp->fd, (genericptr_t) &tmp, sizeof(tmp));
+            msg[slen] = '\0';
+            gamelog_add(tmp.flags, tmp.turn, msg);
+        }
+    }
+}
+
 static void
 restore_msghistory(NHFILE* nhfp)
 {
index b4bec07207ba72424601719a05145d45d4672e02..c34826d08c0f52a248cdd51825fcad09c9c55a42 100644 (file)
@@ -25,6 +25,7 @@ static void saveobjchn(NHFILE *,struct obj **);
 static void savemon(NHFILE *,struct monst *);
 static void savemonchn(NHFILE *,struct monst *);
 static void savetrapchn(NHFILE *,struct trap *);
+static void save_gamelog(NHFILE *);
 static void savegamestate(NHFILE *);
 static void save_msghistory(NHFILE *);
 
@@ -227,6 +228,38 @@ dosave0(void)
     return res;
 }
 
+static void
+save_gamelog(NHFILE* nhfp)
+{
+    struct gamelog_line *tmp = g.gamelog, *tmp2;
+    int slen;
+
+    while (tmp) {
+        tmp2 = tmp->next;
+        if (perform_bwrite(nhfp)) {
+            if (nhfp->structlevel) {
+                slen = strlen(tmp->text);
+                bwrite(nhfp->fd, (genericptr_t) &slen, sizeof(slen));
+                bwrite(nhfp->fd, (genericptr_t) tmp->text, slen);
+                bwrite(nhfp->fd, (genericptr_t) tmp, sizeof(struct gamelog_line));
+            }
+        }
+        if (release_data(nhfp)) {
+            free((genericptr_t) tmp->text);
+            free((genericptr_t) tmp);
+        }
+        tmp = tmp2;
+    }
+    if (perform_bwrite(nhfp)) {
+        if (nhfp->structlevel) {
+            slen = -1;
+            bwrite(nhfp->fd, (genericptr_t) &slen, sizeof(slen));
+        }
+    }
+    if (release_data(nhfp))
+        g.gamelog = NULL;
+}
+
 static void
 savegamestate(NHFILE* nhfp)
 {
@@ -312,6 +345,7 @@ savegamestate(NHFILE* nhfp)
     savenames(nhfp);
     save_waterlevel(nhfp);
     save_msghistory(nhfp);
+    save_gamelog(nhfp);
     if (nhfp->structlevel)
         bflush(nhfp->fd);
     g.program_state.saving--;
index 3175eb60eed56e6ad3da643b6e7ed2c96501627b..9f9c4be8c8212334bcd7b2fff7e3dda2cf493452 100644 (file)
--- a/src/shk.c
+++ b/src/shk.c
@@ -513,6 +513,10 @@ rob_shop(struct monst* shkp)
     /* by this point, we know an actual robbery has taken place */
     eshkp->robbed += total;
     You("stole %ld %s worth of merchandise.", total, currency(total));
+    livelog_printf(LL_ACHIEVE, "stole %ld %s worth of merchandise from %s %s",
+                   total, currency(total), s_suffix(shkname(shkp)),
+                   shtypes[eshkp->shoptype - SHOPBASE].name);
+
     if (!Role_if(PM_ROGUE)) /* stealing is unlawful */
         adjalign(-sgn(u.ualign.type));
 
index 6a204e2dacc42c2baee4fb9e533ffb86b5451aff..607d165971d7052031e0e8f30b798aec3aae96a9 100644 (file)
@@ -492,7 +492,10 @@ study_book(register struct obj* spellbook)
 
             if (read_tribute("books", tribtitle, 0, (char *) 0, 0,
                              spellbook->o_id)) {
-                u.uconduct.literate++;
+                if (!u.uconduct.literate++)
+                    livelog_printf(LL_CONDUCT,
+                                   "became literate by reading %s", tribtitle);
+
                 check_unpaid(spellbook);
                 makeknown(booktype);
                 if (!u.uevent.read_tribute) {
index 54ef241bb41eb6a0787fb3352ce820c791b7fa7d..100749a8c6a47809253bd4f03a59ae71f4498079 100644 (file)
--- a/src/sys.c
+++ b/src/sys.c
@@ -41,6 +41,7 @@ sys_early_init(void)
     sysopt.genericusers = (char *) 0;
     sysopt.maxplayers = 0; /* XXX eventually replace MAX_NR_OF_PLAYERS */
     sysopt.bones_pools = 0;
+    sysopt.livelog = LL_NONE;
 
     /* record file */
     sysopt.persmax = max(PERSMAX, 1);
index b967e51b1c4fb6c11d7e5d303ec38203e0eeefb4..4638351c0b44b296ad98d7a2901ac82d4559372d 100644 (file)
@@ -568,6 +568,10 @@ known_hitum(
             if (mon->wormno && *mhit)
                 cutworm(mon, g.bhitpos.x, g.bhitpos.y, slice_or_chop);
         }
+        if (u.uconduct.weaphit && !oldweaphit)
+            livelog_printf(LL_CONDUCT,
+                                 "hit with a wielded weapon for the first time");
+
     }
     return malive;
 }
index 7cfd5f0f183eea63e4924647fe7c56dafb438488..7cea8423570240607a4d440fa9c002b4249594cc 100644 (file)
@@ -231,7 +231,9 @@ dowrite(struct obj *pen)
     }
 
     /* KMH, conduct */
-    u.uconduct.literate++;
+    if (!u.uconduct.literate++)
+        livelog_printf(LL_CONDUCT,
+                       "became literate by writing %s", an(typeword));
 
     new_obj = mksobj(i, FALSE, FALSE);
     new_obj->bknown = (paper->bknown && pen->bknown);
index af1adc07cdd029eac47cbf572a0ed0ac06d75dc3..ab928d90ff77b8c66eab0a5792f86fd1ddcf1c17 100644 (file)
--- a/src/zap.c
+++ b/src/zap.c
@@ -2031,7 +2031,9 @@ bhito(struct obj *obj, struct obj *otmp)
                 break;
             }
             /* KMH, conduct */
-            u.uconduct.polypiles++;
+            if (!u.uconduct.polypiles++)
+                livelog_printf(LL_CONDUCT, "polymorphed %s first object", uhis());
+
             /* any saved lock context will be dangerously obsolete */
             if (Is_box(obj))
                 (void) boxlock(obj, otmp);
@@ -5520,8 +5522,10 @@ makewish(void)
 {
     char buf[BUFSZ] = DUMMY;
     char promptbuf[BUFSZ];
+    char bufcpy[BUFSZ];
     struct obj *otmp, nothing;
     int tries = 0;
+    int prev_artwish = u.uconduct.wisharti;
 
     promptbuf[0] = '\0';
     nothing = cg.zeroobj; /* lint suppression; only its address matters */
@@ -5546,6 +5550,7 @@ makewish(void)
      *  otmp == &zeroobj.  That includes an artifact which has been denied.
      *  Wishing for "nothing" requires a separate value to remain distinct.
      */
+    strcpy(bufcpy, buf);
     otmp = readobjnam(buf, &nothing);
     if (!otmp) {
         pline("Nothing fitting that description exists in the game.");
@@ -5562,7 +5567,15 @@ makewish(void)
     }
 
     /* KMH, conduct */
-    u.uconduct.wishes++;
+    if (!u.uconduct.wishes++)
+        livelog_printf(LL_CONDUCT | LL_WISH | (prev_artwish < u.uconduct.wisharti ? LL_ARTIFACT : 0),
+                       "made %s first wish - \"%s\"", uhis(), bufcpy);
+    else if (!prev_artwish && u.uconduct.wisharti) /* arti conduct handled in readobjnam() above */
+        livelog_printf(LL_CONDUCT | LL_WISH | LL_ARTIFACT, "made %s first artifact wish - \"%s\"",
+                       uhis(), bufcpy);
+    else
+        livelog_printf(LL_WISH | (prev_artwish < u.uconduct.wisharti ? LL_ARTIFACT : 0),
+                       "wished for \"%s\"", bufcpy);
 
     if (otmp != &cg.zeroobj) {
         const char
index d2d00b6fbd60ee67cf75034ac3e3194907e5c763..89ba178684e8328057fc635999c4aa5578a1f36b 100644 (file)
@@ -335,10 +335,11 @@ install: rootcheck $(GAME) recover $(VARDAT) spec_levs
 # set up the game files
        ( $(MAKE) dofiles )
 # set up some additional files
-       touch $(VARDIR)/perm $(VARDIR)/record $(VARDIR)/logfile $(VARDIR)/xlogfile
-       -( cd $(VARDIR) ; $(CHOWN) $(GAMEUID) perm record logfile xlogfile ; \
-                       $(CHGRP) $(GAMEGRP) perm record logfile xlogfile ; \
-                       chmod $(VARFILEPERM) perm record logfile xlogfile )
+       touch $(VARDIR)/perm $(VARDIR)/record $(VARDIR)/logfile $(VARDIR)/xlogfile \
+         $(VARDIR)/livelog
+       -( cd $(VARDIR) ; $(CHOWN) $(GAMEUID) perm record logfile xlogfile livelog ; \
+                       $(CHGRP) $(GAMEGRP) perm record logfile xlogfile livelog ; \
+                       chmod $(VARFILEPERM) perm record logfile xlogfile livelog )
        true; $(POSTINSTALL)
 # and a reminder
        @echo You may also want to reinstall the man pages via the doc Makefile.
index a4eddb963c88d017fd904ff91a528b45d36b4fa2..2cb19998c65a9ed3b808a104ea41b78851f52f2c 100644 (file)
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                        shellPath = /bin/sh;
-                       shellScript = "mkdir -p \"${NH_INSTALL_DIR}\"/save\ncd \"${NH_DAT_DIR}\"\ncp nhdat license symbols \"${NH_INSTALL_DIR}\"\ncp \"${NH_SRC_DIR}\"/nethack \"${NH_INSTALL_DIR}\"\ncp \"${NH_UTIL_DIR}\"/recover \"${NH_INSTALL_DIR}\"\ntouch \"${NH_INSTALL_DIR}\"/perm\ntouch \"${NH_INSTALL_DIR}\"/record\ntouch \"${NH_INSTALL_DIR}\"/logfile\ntouch \"${NH_INSTALL_DIR}\"/xlogfile\ncd \"${NH_UNIX_DIR}\"\nsh hints/macosx.sh editsysconf sysconf \"${NH_INSTALL_DIR}\"/sysconf\n";
+                       shellScript = "mkdir -p \"${NH_INSTALL_DIR}\"/save\ncd \"${NH_DAT_DIR}\"\ncp nhdat license symbols \"${NH_INSTALL_DIR}\"\ncp \"${NH_SRC_DIR}\"/nethack \"${NH_INSTALL_DIR}\"\ncp \"${NH_UTIL_DIR}\"/recover \"${NH_INSTALL_DIR}\"\ntouch \"${NH_INSTALL_DIR}\"/perm\ntouch \"${NH_INSTALL_DIR}\"/record\ntouch \"${NH_INSTALL_DIR}\"/logfile\ntouch \"${NH_INSTALL_DIR}\"/xlogfile\ntouch \"${NH_INSTALL_DIR}\"/livelog\ncd \"${NH_UNIX_DIR}\"\nsh hints/macosx.sh editsysconf sysconf \"${NH_INSTALL_DIR}\"/sysconf\n";
                };
                3192867121A3A2D500325BEB /* Copy nethack */ = {
                        isa = PBXShellScriptBuildPhase;
index 72be3ec6ad365cebe7dab7be3ce6d3ebe42d7ef3..57861d144e30b81c448816237ccff0ccd26b8cd3 100644 (file)
@@ -52,6 +52,7 @@ $(WASM_DATA_DIR): $(WASM_DATA_DIR)/nhdat
        touch $(WASM_DATA_DIR)/record
        touch $(WASM_DATA_DIR)/logfile
        touch $(WASM_DATA_DIR)/xlogfile
+       touch $(WASM_DATA_DIR)/livelog
        cp ../sys/libnh/sysconf $(WASM_DATA_DIR)/sysconf
 
 $(WASM_DATA_DIR)/nhdat:
index e3f9a9f618b3153d7426eca143dd4efb4ba2f256..bfa1ca6d0c06c457b181184a2ccacd57e505f374 100755 (executable)
@@ -103,6 +103,8 @@ NHCFLAGS+=-DCOMPRESS=\"/bin/gzip\" -DCOMPRESS_EXTENSION=\".gz\"
 #NHCFLAGS+=-DMSGHANDLER
 #NHCFLAGS+=-DTTY_TILES_ESCCODES
 #NHCFLAGS+=-DTTY_SOUND_ESCCODES
+#NHCFLAGS+=-DNO_CHRONICLE
+#NHCFLAGS+=-DLIVELOG
 
 CFLAGS+= $(WINCFLAGS)    #WINCFLAGS set from multiw-2.370
 CFLAGS+= $(NHCFLAGS)
index e26aa28dd669c1206d24912e0e6b078c370f95dc..088b3cdd45f07697adb37a4bae5fee0278d779df 100755 (executable)
@@ -103,6 +103,8 @@ NHCFLAGS+=-DNOMAIL
 #NHCFLAGS+=-DMSGHANDLER
 #NHCFLAGS+=-DTTY_TILES_ESCCODES
 #NHCFLAGS+=-DTTY_SOUND_ESCCODES
+#NHCFLAGS+=-DNO_CHRONICLE
+#NHCFLAGS+=-DLIVELOG
 
 CFLAGS+= $(WINCFLAGS)  #WINCFLAGS set from multiw-2.370
 CFLAGS+= $(NHCFLAGS)
@@ -449,7 +451,7 @@ build_package_root:
        sys/unix/hints/macosx.sh editsysconf sys/unix/sysconf $(PKGROOT_UGLN)/sysconf
        cd dat; install -p $(DATNODLB) ../$(PKGROOT_UGLN)
 # XXX these files should be somewhere else for good Mac form
-       touch $(PKGROOT_UGLN)/perm $(PKGROOT_UGLN)/record $(PKGROOT_UGLN)/logfile $(PKGROOT_UGLN)/xlogfile
+       touch $(PKGROOT_UGLN)/perm $(PKGROOT_UGLN)/record $(PKGROOT_UGLN)/logfile $(PKGROOT_UGLN)/xlogfile $(PKGROOT_UGLN)/livelog
        mkdir $(PKGROOT_UGLN)/save
 # XXX what about a news file?
 
@@ -473,6 +475,7 @@ build_package_root:
        echo chmod $(VARFILEPERM) $(HACKDIR)/record   >> PKGSCRIPTS/postinstall
        echo chmod $(VARFILEPERM) $(HACKDIR)/logfile  >> PKGSCRIPTS/postinstall
        echo chmod $(VARFILEPERM) $(HACKDIR)/xlogfile >> PKGSCRIPTS/postinstall
+       echo chmod $(VARFILEPERM) $(HACKDIR)/livelog  >> PKGSCRIPTS/postinstall
        echo chmod $(VARFILEPERM) $(HACKDIR)/sysconf  >> PKGSCRIPTS/postinstall
        echo chmod $(GAMEPERM)   $(SHELLDIR)/nethack  >> PKGSCRIPTS/postinstall
        echo chmod $(EXEPERM)    $(SHELLDIR)/recover  >> PKGSCRIPTS/postinstall
index c62852522a83230289552025900be3c894b3ce00..b335bb503fd6c15797b79d965500e0b98810f8e4 100644 (file)
@@ -86,6 +86,29 @@ MAXPLAYERS=10
 # Maximum number of score file entries to use for random statue names
 #MAX_STATUENAME_RANK=10
 
+# Use "Live logging" for important events (achievements, wishes, etc)
+# Only available if NetHack was compiled with LIVELOG.
+# Only really meaningful for public servers.
+# See the log in-game with #chronicle -command.
+# Bitmask for kinds of things you want to log - combine the following values
+# as desired.
+# 0x0000 - No live logging (default)
+# 0x0001 - Wishes
+# 0x0002 - Significant achievements (complete sokoban, perform invocation, etc)
+# 0x0004 - Kill, destroy or bribe a unique monster.
+# 0x0008 - Significant religious events (sacrifice gifts, crowning)
+# 0x0010 - Life-saving
+# 0x0020 - Break conduct - see also LLC_TURNS below.
+# 0x0040 - Artifact obtained (#name Sting, dip for Excalibur)
+# 0x0080 - Genocides
+# 0x0100 - Murder of tame pet
+# 0x0200 - Changed alignment temporarily or permanently
+# 0x0400 - Log URL for dumplog if ascended
+# 0x0800 - Log dumplog url for all games
+# 0x1000 - Log 'minor' achievements - can be spammy
+# 0x8000 - Livelog debug msgs (currently only 'enter new level')
+#LIVELOG=0x1FFF
+
 # Show debugging information originating from these source files.
 # Use '*' for all, or list source files separated by spaces.
 # Only available if game has been compiled with DEBUG, and can be