From c220af0bf608ca2b192a65ccd33089a4a01c3797 Mon Sep 17 00:00:00 2001 From: fish4terrisa-MSDSM Date: Thu, 11 Dec 2025 21:20:44 +0800 Subject: [PATCH 1/2] Improved #loot command to allow the player to exchange items with their pets - Now players can use #loot command on their pets to exchange items with their pets. It basically functions the same as #loot in xNetHack. --- libnethack/include/extern.h | 5 + libnethack/src/dogmove.c | 3 +- libnethack/src/mon.c | 11 ++ libnethack/src/pickup.c | 239 +++++++++++++++++++++++++++++++----- libnethack/src/weapon.c | 12 ++ libnethack/src/wield.c | 10 ++ libnethack/src/worn.c | 51 ++++++++ 7 files changed, 296 insertions(+), 35 deletions(-) diff --git a/libnethack/include/extern.h b/libnethack/include/extern.h index 443dc95e9..df8053e7b 100644 --- a/libnethack/include/extern.h +++ b/libnethack/include/extern.h @@ -429,6 +429,7 @@ extern void wary_dog(struct monst *, boolean); /* ### dogmove.c ### */ +extern struct obj *DROPPABLES(struct monst *); extern int dog_nutrition(struct monst *, struct obj *); extern int dog_eat(struct monst *, struct obj *, int, int, boolean); extern int dog_move(struct monst *, int); @@ -1278,6 +1279,7 @@ extern boolean angry_guards(boolean); extern void pacify_guards(void); extern long mm_aggression(const struct monst *, const struct monst *, boolean); extern boolean grudge(const struct permonst *, const struct permonst *); +extern void check_gear_next_turn(struct monst *); /* ### mondata.c ### */ @@ -2151,11 +2153,13 @@ extern int can_twoweapon(void); extern void drop_uswapwep(void); extern int dotwoweapon(const struct nh_cmd_arg *); extern void uwepgone(void); +extern void mwepgone(struct monst *); extern void uswapwepgone(void); extern void uqwepgone(void); extern void untwoweapon(void); extern int chwepon(struct monst *, struct obj *, int); extern int welded(struct obj *); +extern int mwelded(struct obj *); extern void weldmsg(enum msg_channel, struct obj *); extern void setmnotwielded(struct monst *, struct obj *); extern void unwield_weapons_silently(void); @@ -2238,6 +2242,7 @@ extern void m_dowear(struct monst *, boolean); extern struct obj *which_armor(const struct monst *, enum objslot); extern void mon_break_armor(struct monst *, boolean); extern int racial_exception(struct monst *, struct obj *); +extern void extract_from_minvent(struct monst *, struct obj *, boolean, boolean); extern int extra_pref(const struct monst *, struct obj *); /* ### write.c ### */ diff --git a/libnethack/src/dogmove.c b/libnethack/src/dogmove.c index ca827f6a7..2241c94e9 100644 --- a/libnethack/src/dogmove.c +++ b/libnethack/src/dogmove.c @@ -13,7 +13,6 @@ static boolean dog_hunger(struct monst *, struct edog *); static int dog_invent(struct monst *, struct edog *, int); static int dog_goal(struct monst *, struct edog *, int, int, int); -static struct obj *DROPPABLES(struct monst *); static boolean can_reach_location(struct monst *, xchar, xchar, xchar, xchar); static boolean could_reach_item(struct monst *, xchar, xchar); static boolean is_better_armor(const struct monst *mtmp, struct obj *otmp); @@ -186,7 +185,7 @@ pet_wants_object(const struct pet_weapons *p, struct obj *obj) return FALSE; } -static struct obj * +struct obj * DROPPABLES(struct monst *mon) { struct obj *obj; diff --git a/libnethack/src/mon.c b/libnethack/src/mon.c index 1ff8c5ecc..f6ddc48cf 100644 --- a/libnethack/src/mon.c +++ b/libnethack/src/mon.c @@ -4416,4 +4416,15 @@ mimic_hit_msg(struct monst *mtmp, short otyp) } } +/* setting misc_worn_check's I_SPECIAL bit flags a monster to reassess + and potentially re-equip gear at the start of its next move; + this hides the details of that */ +void +check_gear_next_turn(struct monst *mon) +{ + mon->misc_worn_check |= W_MASKABLE; + mon->misc_worn_check |= W_RING; + mon->misc_worn_check |= W_ARTIFACT; +} + /*mon.c*/ diff --git a/libnethack/src/pickup.c b/libnethack/src/pickup.c index 03761e5f6..d7cb575a7 100644 --- a/libnethack/src/pickup.c +++ b/libnethack/src/pickup.c @@ -1507,6 +1507,194 @@ doloot(const struct nh_cmd_arg *arg) return timepassed; } +/* Give or take items from mtmp. + * Assumes the hero can see mtmp as a monster in its natural state. + * Return the amount of time passed */ +static int +exchange_objects_with_mon(struct monst *mtmp, boolean taking) +{ + int i, n, transferred = 0, time_taken = 1; + struct object_pick *pick_list = NULL; + const char *qstr = taking ? "Take what?" : "Give what?"; + + if (taking && !mtmp->minvent) { + pline(msgc_cancelled, "%s isn't carrying anything.", Monnam(mtmp)); + return 0; + } + else if (!taking && !youmonst.minvent) { + pline(msgc_cancelled, "You aren't carrying anything."); + return 0; + } + if (mtmp->msleeping || mtmp->mfrozen || !mtmp->mcanmove) { + pline(msgc_cancelled, "%s doesn't respond.", Monnam(mtmp)); + return 0; + } + if (mtmp->meating) { + pline(msgc_cancelled, "%s is eating noisily.", Monnam(mtmp)); + return 0; + } + + n = query_objlist(qstr, taking ? mtmp->minvent : youmonst.minvent, + INVORDER_SORT | (taking ? 0 : USE_INVLET), + &pick_list, PICK_ANY, allow_all); + + for (i = 0; i < n; ++i) { + struct obj* otmp = pick_list[i].obj; + long maxquan = min(pick_list[i].count, otmp->quan); + long unwornmask = otmp->owornmask; + boolean petri = (otmp->otyp == CORPSE + && touch_petrifies(&mons[otmp->corpsenm])); + boolean mtmp_would_ston = (!taking && petri + && !which_armor(mtmp, W_ARMOR) + && !resists_ston(mtmp)); + + /* Clear inapplicable wornmask bits */ + unwornmask &= ~(W_MASK(os_carried) | W_MASK(os_invoked) | W_MASK(os_quiver)); + + if (!taking) { + int carryamt; + if (welded(otmp)) { + weldmsg(msgc_cancelled1, otmp); + continue; + } + if (!canletgo(otmp, "give away")) { + /* this prints its own messages */ + continue; + } + if (!mindless(mtmp->data) && mtmp_would_ston) { + pline(msgc_failcurse, "%s refuses to take %s%s.", Monnam(mtmp), + maxquan < otmp->quan ? "any of " : "", yname(otmp)); + continue; + } + if (otmp == uball || otmp == uchain) { + /* you can't give a monster your ball & chain, because it + * causes problems elsewhere... */ + pline(msgc_failcurse, "%s shackled to your %s and cannot be given away.", + Tobjnam(otmp, "are"), body_part(LEG)); + continue; + } + carryamt = can_carry(mtmp, otmp); + if (nohands(mtmp->data) && DROPPABLES(mtmp)) { + carryamt = 0; + } + if (carryamt == 0) { + /* note: this includes both "can't carry" and "won't carry", but + * doesn't distinguish them */ + pline(msgc_failcurse, "%s can't carry %s%s.", Monnam(mtmp), + maxquan < otmp->quan ? "any of " : "", yname(otmp)); + /* debatable whether to continue or break here; if the player + * overloads the monster with too many items, breaking would be + * preferable, but if they just can't take this one otmp for + * whatever reason, we should continue instead. It remains to + * be seen which is the more common scenario. */ + continue; + } + else if (carryamt < maxquan) { + pline(msgc_failcurse, "%s can only carry %s of %s.", Monnam(mtmp), + carryamt > 1 ? "some" : "one", yname(otmp)); + maxquan = carryamt; + } + if (maxquan < otmp->quan) { + otmp = splitobj(otmp, maxquan); + } + pline(msgc_actionboring, "You give %s %s.", mon_nam(mtmp), yname(otmp)); + if (otmp->owornmask) { + setnotworn(otmp); /* reset quivered, wielded, etc, status */ + } + obj_extract_self(otmp); + if (add_to_minv(mtmp, otmp, NULL)) { + otmp = (struct obj *) 0; /* merged with something in minvent */ + } + transferred++; + /* Possible extension: if you give edible food to a pet, it should + * eat it directly. But that should probably go into the pet AI + * code, not here. */ + } + else { + /* cursed weapons, armor, accessories, etc treated the same */ + if ((otmp->cursed && (unwornmask & ~W_MASK(os_wep))) + || mwelded(otmp)) { + pline(msgc_failcurse, "%s won't come off!", Yname2(otmp)); + otmp->bknown = 1; + continue; + } + if (unwornmask & (W_WORN | W_MASK(os_saddle))) { + int m_delay = objects[otmp->otyp].oc_delay; + if ((unwornmask & (W_MASK(os_arm) | W_MASK(os_armu))) != 0L + && (mtmp->misc_worn_check & W_MASK(os_armc)) != 0L) { + /* extra delay for removing a cloak */ + m_delay += 2; + } + if ((unwornmask & W_MASK(os_saddle)) != 0L) { + if (flags.verbose) + pline(msgc_actionboring, "You take %s off of %s.", + xname(otmp), mon_nam(mtmp)); + /* unstrapping a saddle takes additional time */ + time_taken += rn2(3); + } + else { + pline(msgc_actionboring, "%s %s %s %s.", Monnam(mtmp), + m_delay > 1 ? "begins removing" : "removes", + mhis(mtmp), xname(otmp)); + } + mtmp->mfrozen = m_delay; + /* unwear the item now */ + update_property_for_oprops(mtmp, otmp, which_slot(otmp)); + if (mtmp->mfrozen) { /* might be 0 */ + mtmp->mcanmove = 0; + otmp->owornmask = 0L; + /* normally extract_from_minvent handles this stuff, but + * since we are setting owornmask to 0 now we have to + * do it here. */ + otmp->owt = weight(otmp); /* reset armor weight */ + mtmp->misc_worn_check &= ~unwornmask; + /* monster is now occupied, won't hand over other things */ + break; + } + /* This isn't an ideal solution, since there's no way to + * communicate directly to the player when the monster unfreezes + * that it is done taking the item off. They also could try to + * rewear it soon after they begin moving again. + * The alternative is to make this an occupation: the hero + * stands next to the monster for the duration of its disrobing, + * and assuming they're both still in place at the end, the hero + * is given the item directly. But that's more complex and has + * a lot more edge cases; this may suffice. */ + } + if (maxquan < otmp->quan) { + otmp = splitobj(otmp, maxquan); + } + extract_from_minvent(mtmp, otmp, TRUE, TRUE); + if (*in_rooms(level, mtmp->mx, mtmp->my, SHOPBASE)) { + addtobill(otmp, FALSE, FALSE, FALSE); + } + otmp = hold_another_object(otmp, "You take, but drop, %s.", + doname(otmp), "You take: "); + transferred++; + } + if (otmp && petri) { + if (taking && !uarmg && !Stone_resistance) { + instapetrify(corpse_xname(otmp, + FALSE)); + break; /* if life-saved, stop taking items */ + } + else if (mtmp_would_ston) { + minstapetrify(&youmonst, mtmp); + break; + } + } + } + free(pick_list); + if (transferred > 0) { + /* They might have gained some gear they would want to wear, or lost + * some and now have a different option. Reassess next turn and see. */ + check_gear_next_turn(mtmp); + } + /* time_taken is 1 for normal item(s), rnd(3) if you removed a saddle */ + return (n > 0 ? time_taken : 0); +} + + /* loot_mon() returns amount of time passed. */ int loot_mon(struct monst *mtmp, int *passed_info, boolean * prev_loot) @@ -1516,47 +1704,32 @@ loot_mon(struct monst *mtmp, int *passed_info, boolean * prev_loot) struct obj *otmp; const char *qbuf; - /* 3.3.1 introduced the ability to remove saddle from a steed */ - /* *passed_info is set to TRUE if a loot query was given. */ - /* *prev_loot is set to TRUE if something was actually acquired in here. */ - if (mtmp && mtmp != u.usteed && (otmp = which_armor(mtmp, os_saddle))) { - long unwornmask; - + if (mtmp && (mtmp->mtame || wizard) && canspotmon(mtmp) + && !(mtmp->mundetected || mtmp->m_ap_type)) { + /* Possible future extension: using this to steal items from peaceful + * and hostile monsters. */ if (passed_info) *passed_info = 1; - qbuf = msgprintf("Do you want to remove the saddle from %s?", - x_monnam(mtmp, ARTICLE_THE, NULL, - SUPPRESS_SADDLE, FALSE)); + qbuf = msgprintf("Do you want to take something from %s?", mon_nam(mtmp)); if ((c = yn_function(qbuf, ynqchars, 'n')) == 'y') { - if (nolimbs(youmonst.data)) { - pline(msgc_cancelled, "You can't do that without limbs."); - /* not body_part(HAND); we're talking about an appendage that - we /don't/ have */ - return 0; - } - if (otmp->cursed) { - pline(otmp->bknown ? msgc_cancelled1 : msgc_failcurse, - "You can't. The saddle seems to be stuck to %s.", - x_monnam(mtmp, ARTICLE_THE, NULL, SUPPRESS_SADDLE, - FALSE)); - otmp->bknown = TRUE; - /* the attempt costs you time */ - return 1; - } - obj_extract_self(otmp); - if ((unwornmask = otmp->owornmask) != 0L) { - mtmp->misc_worn_check &= ~unwornmask; - otmp->owornmask = 0L; - update_property(mtmp, objects[otmp->otyp].oc_oprop, which_slot(otmp)); - } - hold_another_object(otmp, "You drop %s!", doname(otmp), NULL); - timepassed = rnd(3); if (prev_loot) *prev_loot = TRUE; - } else if (c == 'q') { + return exchange_objects_with_mon(mtmp, TRUE); + } + else if (c == 'q') { + return 0; + } + qbuf = msgprintf("Do you want to give something to %s?", mon_nam(mtmp)); + if ((c = yn_function(qbuf, ynqchars, 'n')) == 'y') { + if (prev_loot) + *prev_loot = TRUE; + return exchange_objects_with_mon(mtmp, FALSE); + } + else { /* 'n' or 'q' */ return 0; } } + /* 3.4.0 introduced the ability to pick things up from within swallower's stomach */ if (Engulfed) { diff --git a/libnethack/src/weapon.c b/libnethack/src/weapon.c index 18b3ae540..70d667a6a 100644 --- a/libnethack/src/weapon.c +++ b/libnethack/src/weapon.c @@ -800,6 +800,18 @@ mon_wield_item(struct monst *mon) return 0; } +/* force monster to stop wielding current weapon, if any */ +void +mwepgone(struct monst *mon) +{ + struct obj *mwep = MON_WEP(mon); + + if (mwep) { + setmnotwielded(mon, mwep); + mon->weapon_check = NEED_WEAPON; + } +} + /* attack bonus for strength & dexterity */ int abon(void) diff --git a/libnethack/src/wield.c b/libnethack/src/wield.c index 7270a6e17..553f41eba 100644 --- a/libnethack/src/wield.c +++ b/libnethack/src/wield.c @@ -719,6 +719,16 @@ weldmsg(enum msg_channel msgc, struct obj *obj) (const char *)makeplural(body_part(HAND)) : body_part(HAND)); } +/* test whether monster's wielded weapon is stuck to hand/paw/whatever */ +int +mwelded(struct obj *obj) +{ + /* caller is responsible for making sure this is a monster's item */ + if (obj && (obj->owornmask & W_MASK(os_wep)) && will_weld(obj)) + return 1; + return 0; +} + /* Unwields all weapons silently. */ void unwield_weapons_silently(void) diff --git a/libnethack/src/worn.c b/libnethack/src/worn.c index 887f184ac..f862c0200 100644 --- a/libnethack/src/worn.c +++ b/libnethack/src/worn.c @@ -891,4 +891,55 @@ racial_exception(struct monst *mon, struct obj *obj) return 0; } +/* Remove an object from a monster's inventory. */ +void +extract_from_minvent( + struct monst *mon, + struct obj *obj, + boolean do_extrinsics, /* whether to call update_mon_extrinsics */ + boolean silently) /* doesn't affect all possible messages, + * just update_mon_extrinsics's */ +{ + long unwornmask = obj->owornmask; + + /* + * At its core this is just obj_extract_self(), but it also handles + * any updates that need to happen if the gear is equipped or in + * some other sort of state that needs handling. + * Note that like obj_extract_self(), this leaves obj free. + */ + + if (obj->where != OBJ_MINVENT) { + impossible("extract_from_minvent called on object not in minvent"); + obj_extract_self(obj); /* free it anyway to avoid a panic */ + return; + } + /* handle gold dragon scales/scale-mail (lit when worn) before clearing + obj->owornmask because artifact_light() expects that to be W_ARM */ + if ((unwornmask & (W_MASK(os_arm) | W_MASK(os_armc))) != 0 && obj->lamplit + && artifact_light(obj)) + end_burn(obj, FALSE); + + obj_extract_self(obj); + obj->owornmask = 0L; + if (unwornmask) { + obj->owt = weight(obj); /* reset armor to base weight */ + if (!DEADMONSTER(mon)) { + if (do_extrinsics) { + //update_mon_extrinsics(mon, obj, FALSE, silently); + update_property_for_oprops(mon, obj, which_slot(obj)); + } + mselftouch(mon, NULL, &youmonst); + } + mon->misc_worn_check &= ~unwornmask; + /* give monster a chance to wear other equipment on its next + move instead of waiting until it picks something up */ + check_gear_next_turn(mon); + } + obj_no_longer_held(obj); + if (unwornmask & W_MASK(os_wep)) { + mwepgone(mon); /* unwields and sets weapon_check to NEED_WEAPON */ + } +} + /*worn.c*/ From 72274da3a404e7fce073d10cdd6c26449386ce25 Mon Sep 17 00:00:00 2001 From: fish4terrisa-MSDSM Date: Thu, 11 Dec 2025 23:18:37 +0800 Subject: [PATCH 2/2] Several fixes for the #loot on pet now the pet will correctly check if it can try to wear a new armor or use another weapon after their current one is gone. Also fixed the previous issue that prevent the player from taking away the weapon from their pet. --- libnethack/src/mon.c | 5 +++++ libnethack/src/pickup.c | 1 + libnethack/src/weapon.c | 2 ++ libnethack/src/worn.c | 6 +++--- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/libnethack/src/mon.c b/libnethack/src/mon.c index f6ddc48cf..8604a1f8e 100644 --- a/libnethack/src/mon.c +++ b/libnethack/src/mon.c @@ -4425,6 +4425,11 @@ check_gear_next_turn(struct monst *mon) mon->misc_worn_check |= W_MASKABLE; mon->misc_worn_check |= W_RING; mon->misc_worn_check |= W_ARTIFACT; + if (attacktype(mon->data, AT_WEAP) && mon->weapon_check == NEED_WEAPON) { + mon->weapon_check = NEED_HTH_WEAPON; + mon_wield_item(mon); + } + m_dowear(mon, FALSE); } /*mon.c*/ diff --git a/libnethack/src/pickup.c b/libnethack/src/pickup.c index d7cb575a7..d1e98fdf3 100644 --- a/libnethack/src/pickup.c +++ b/libnethack/src/pickup.c @@ -1639,6 +1639,7 @@ exchange_objects_with_mon(struct monst *mtmp, boolean taking) } mtmp->mfrozen = m_delay; /* unwear the item now */ + update_property(mtmp, objects[otmp->otyp].oc_oprop, which_slot(otmp)); update_property_for_oprops(mtmp, otmp, which_slot(otmp)); if (mtmp->mfrozen) { /* might be 0 */ mtmp->mcanmove = 0; diff --git a/libnethack/src/weapon.c b/libnethack/src/weapon.c index 70d667a6a..1dc4837c6 100644 --- a/libnethack/src/weapon.c +++ b/libnethack/src/weapon.c @@ -1580,6 +1580,8 @@ setmnotwielded(struct monst *mon, struct obj *obj) s_suffix(mon_nam(mon)), mbodypart(mon, HAND), otense(obj, "stop")); } + if (MON_WEP(mon) == obj) + MON_NOWEP(mon); obj->owornmask &= ~W_MASK(os_wep); } diff --git a/libnethack/src/worn.c b/libnethack/src/worn.c index f862c0200..2e407a813 100644 --- a/libnethack/src/worn.c +++ b/libnethack/src/worn.c @@ -920,23 +920,23 @@ extract_from_minvent( && artifact_light(obj)) end_burn(obj, FALSE); - obj_extract_self(obj); obj->owornmask = 0L; if (unwornmask) { obj->owt = weight(obj); /* reset armor to base weight */ + mon->misc_worn_check &= ~unwornmask; if (!DEADMONSTER(mon)) { if (do_extrinsics) { - //update_mon_extrinsics(mon, obj, FALSE, silently); + update_property(mon, objects[obj->otyp].oc_oprop, which_slot(obj)); update_property_for_oprops(mon, obj, which_slot(obj)); } mselftouch(mon, NULL, &youmonst); } - mon->misc_worn_check &= ~unwornmask; /* give monster a chance to wear other equipment on its next move instead of waiting until it picks something up */ check_gear_next_turn(mon); } obj_no_longer_held(obj); + obj_extract_self(obj); if (unwornmask & W_MASK(os_wep)) { mwepgone(mon); /* unwields and sets weapon_check to NEED_WEAPON */ }