From: Pasi Kallinen Date: Wed, 9 Feb 2022 20:36:19 +0000 (+0200) Subject: Chronicle of major events, and livelog X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=1e90f892031502348913794554f185fdab668f7f;p=nethack Chronicle of major events, and livelog 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. --- diff --git a/doc/fixes3-7-0.txt b/doc/fixes3-7-0.txt index 3c1026ccc..b70838afb 100644 --- a/doc/fixes3-7-0.txt +++ b/doc/fixes3-7-0.txt @@ -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 diff --git a/include/config.h b/include/config.h index 1e7bc353a..29ff94b62 100644 --- a/include/config.h +++ b/include/config.h @@ -173,9 +173,9 @@ /* * 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 diff --git a/include/decl.h b/include/decl.h index dff6387e7..934cc90dd 100644 --- a/include/decl.h +++ b/include/decl.h @@ -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 diff --git a/include/extern.h b/include/extern.h index a2bf6ca7e..4f565a1e6 100644 --- a/include/extern.h +++ b/include/extern.h @@ -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); diff --git a/include/global.h b/include/global.h index 7ee316945..1aea2e939 100644 --- a/include/global.h +++ b/include/global.h @@ -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 */ diff --git a/include/patchlevel.h b/include/patchlevel.h index 7d87ab226..11f6d9d28 100644 --- a/include/patchlevel.h +++ b/include/patchlevel.h @@ -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. diff --git a/include/sys.h b/include/sys.h index ef794e747..78cf47eee 100644 --- a/include/sys.h +++ b/include/sys.h @@ -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; diff --git a/src/attrib.c b/src/attrib.c index bb326ac52..2a2dbf6e9 100644 --- a/src/attrib.c +++ b/src/attrib.c @@ -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"); diff --git a/src/cmd.c b/src/cmd.c index 617084ee5..bcafdd2bd 100644 --- 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", diff --git a/src/decl.c b/src/decl.c index 56514cd45..180b9f7a0 100644 --- a/src/decl.c +++ b/src/decl.c @@ -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 */ diff --git a/src/do.c b/src/do.c index 3f881e3b7..9588fd91f 100644 --- 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 { diff --git a/src/do_name.c b/src/do_name.c index bdc2a8c6d..392a7993a 100644 --- a/src/do_name.c +++ b/src/do_name.c @@ -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)) diff --git a/src/dothrow.c b/src/dothrow.c index c9d07789b..9225fcaa0 100644 --- a/src/dothrow.c +++ b/src/dothrow.c @@ -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); diff --git a/src/eat.c b/src/eat.c index ce264a2ab..8e17c4847 100644 --- 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; } diff --git a/src/end.c b/src/end.c index f842ae038..4ed8d7010 100644 --- 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; } } diff --git a/src/engrave.c b/src/engrave.c index 44ca6b0d3..a3b7db770 100644 --- a/src/engrave.c +++ b/src/engrave.c @@ -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. */ diff --git a/src/files.c b/src/files.c index 0a8276821..55397a61d 100644 --- a/src/files.c +++ b/src/files.c @@ -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*/ diff --git a/src/fountain.c b/src/fountain.c index d7e72d81f..dbc19f887 100644 --- a/src/fountain.c +++ b/src/fountain.c @@ -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; diff --git a/src/hack.c b/src/hack.c index 420ccd676..b15811c62 100644 --- a/src/hack.c +++ b/src/hack.c @@ -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); diff --git a/src/insight.c b/src/insight.c index 5b789418b..077ef6b83 100644 --- a/src/insight.c +++ b/src/insight.c @@ -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 */ diff --git a/src/minion.c b/src/minion.c index 70e281565..1d17386ef 100644 --- a/src/minion.c +++ b/src/minion.c @@ -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; diff --git a/src/mon.c b/src/mon.c index 52c63a74d..137cd2ed5 100644 --- 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); diff --git a/src/pickup.c b/src/pickup.c index 553d7913e..cb4f0f180 100644 --- a/src/pickup.c +++ b/src/pickup.c @@ -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') */ diff --git a/src/pline.c b/src/pline.c index a8cdb7338..17d472006 100644 --- a/src/pline.c +++ b/src/pline.c @@ -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 diff --git a/src/polyself.c b/src/polyself.c index e9d75ce71..a8983e21a 100644 --- a/src/polyself.c +++ b/src/polyself.c @@ -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 */ diff --git a/src/potion.c b/src/potion.c index 172cfd10a..e36324d26 100644 --- a/src/potion.c +++ b/src/potion.c @@ -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); diff --git a/src/pray.c b/src/pray.c index a7158d5fe..061b9f7b2 100644 --- a/src/pray.c +++ b/src/pray.c @@ -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)?] */ diff --git a/src/priest.c b/src/priest.c index d1c6911c4..c14ce4261 100644 --- a/src/priest.c +++ b/src/priest.c @@ -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)); diff --git a/src/read.c b/src/read.c index a177923f9..c944b11f9 100644 --- a/src/read.c +++ b/src/read.c @@ -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, diff --git a/src/restore.c b/src/restore.c index ae1908dcf..59e61d36b 100644 --- a/src/restore.c +++ b/src/restore.c @@ -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) { diff --git a/src/save.c b/src/save.c index b4bec0720..c34826d08 100644 --- a/src/save.c +++ b/src/save.c @@ -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--; diff --git a/src/shk.c b/src/shk.c index 3175eb60e..9f9c4be8c 100644 --- 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)); diff --git a/src/spell.c b/src/spell.c index 6a204e2da..607d16597 100644 --- a/src/spell.c +++ b/src/spell.c @@ -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) { diff --git a/src/sys.c b/src/sys.c index 54ef241bb..100749a8c 100644 --- 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); diff --git a/src/uhitm.c b/src/uhitm.c index b967e51b1..4638351c0 100644 --- a/src/uhitm.c +++ b/src/uhitm.c @@ -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; } diff --git a/src/write.c b/src/write.c index 7cfd5f0f1..7cea84235 100644 --- a/src/write.c +++ b/src/write.c @@ -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); diff --git a/src/zap.c b/src/zap.c index af1adc07c..ab928d90f 100644 --- 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, ¬hing); 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 diff --git a/sys/unix/Makefile.top b/sys/unix/Makefile.top index d2d00b6fb..89ba17868 100644 --- a/sys/unix/Makefile.top +++ b/sys/unix/Makefile.top @@ -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. diff --git a/sys/unix/NetHack.xcodeproj/project.pbxproj b/sys/unix/NetHack.xcodeproj/project.pbxproj index a4eddb963..2cb19998c 100644 --- a/sys/unix/NetHack.xcodeproj/project.pbxproj +++ b/sys/unix/NetHack.xcodeproj/project.pbxproj @@ -1241,7 +1241,7 @@ ); 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; diff --git a/sys/unix/hints/include/cross-post.370 b/sys/unix/hints/include/cross-post.370 index 72be3ec6a..57861d144 100644 --- a/sys/unix/hints/include/cross-post.370 +++ b/sys/unix/hints/include/cross-post.370 @@ -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: diff --git a/sys/unix/hints/linux.370 b/sys/unix/hints/linux.370 index e3f9a9f61..bfa1ca6d0 100755 --- a/sys/unix/hints/linux.370 +++ b/sys/unix/hints/linux.370 @@ -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) diff --git a/sys/unix/hints/macOS.370 b/sys/unix/hints/macOS.370 index e26aa28dd..088b3cdd4 100755 --- a/sys/unix/hints/macOS.370 +++ b/sys/unix/hints/macOS.370 @@ -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 diff --git a/sys/unix/sysconf b/sys/unix/sysconf index c62852522..b335bb503 100644 --- a/sys/unix/sysconf +++ b/sys/unix/sysconf @@ -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