tinymux/mux/modules/engine/create.cpp

1360 lines
34 KiB
C++
Raw Permalink Normal View History

/*! \file create.cpp
* \brief Commands that create new objects.
*
* This affects creation and destruction of all object types as well as their
* initial and final relationships with other objects.
*/
#include "copyright.h"
#include "autoconf.h"
#include "config.h"
#include "externs.h"
#include "routing.h"
// ---------------------------------------------------------------------------
// parse_linkable_room: Get a location to link to.
//
static dbref parse_linkable_room(const dbref player, const UTF8 *room_name)
{
init_match(player, room_name, NOTYPE);
match_everything(MAT_NO_EXITS | MAT_NUMERIC | MAT_HOME);
const dbref room = match_result();
// HOME is always linkable
//
if (room == HOME)
{
return HOME;
}
// Make sure we can link to it
//
if (!Good_obj(room))
{
notify_quiet(player, M_("Thats not a valid object."));
return NOTHING;
}
else if ( !Has_contents(room)
|| !Linkable(player, room))
{
notify_quiet(player, M_("You cant link to that."));
return NOTHING;
}
else
{
return room;
}
}
// ---------------------------------------------------------------------------
// open_exit, do_open: Open a new exit and optionally link it somewhere.
//
static void open_exit(const dbref player, dbref loc, UTF8 *direction, UTF8 *linkto)
{
if (!Good_obj(loc))
{
return;
}
2018-10-03 17:54:51 +00:00
if ( nullptr == direction
|| '\0' == direction[0])
{
notify_quiet(player, M_("Open where?"));
return;
}
else if (!Controls(player, loc))
{
if ( !( Open_ok(loc)
&& could_doit(player, loc, A_LOPEN)))
{
notify_quiet(player, NOPERM_MESSAGE);
return;
}
}
const dbref exit = create_obj(player, TYPE_EXIT, direction, 0);
if (NOTHING == exit)
{
return;
}
// Initialize everything and link it in.
//
s_Exits(exit, loc);
s_Next(exit, Exits(loc));
s_Exits(loc, exit);
local_data_create(exit);
ServerEventsSinkNode *p = g_pServerEventsSinkListHead;
2018-10-03 17:54:51 +00:00
while (nullptr != p)
{
p->pSink->data_create(exit);
p = p->pNext;
}
// and we're done
//
notify_quiet(player, M_("Opened."));
// See if we should do a link
//
if (!linkto || !*linkto)
{
return;
}
loc = parse_linkable_room(player, linkto);
if (Good_obj(loc) || loc == HOME)
{
// Make sure the player passes the link lock
//
if ( !Link_Anywhere(player)
&& !could_doit(player, loc, A_LLINK))
{
notify_quiet(player, M_("You cant link to there."));
return;
}
// Link it if the player can pay for it
//
if (!payfor(player, mudconf.linkcost))
{
notify_quiet(player,
tprintf(M_("You dont have enough %s to link."),
mudconf.many_coins));
}
else
{
s_Location(exit, loc);
notify_quiet(player, M_("Linked."));
route_invalidate();
}
}
}
void do_open(const dbref executor, const dbref caller, const dbref enactor, const int eval, const int key,
UTF8 *direction, UTF8 *links[], int nlinks, const UTF8 *cargs[], const int ncargs)
{
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
UTF8 *dest;
// Create the exit and link to the destination, if there is one
//
if (nlinks >= 1)
{
dest = links[0];
}
else
{
2018-10-03 17:54:51 +00:00
dest = nullptr;
}
dbref loc;
if (key == OPEN_INVENTORY)
{
loc = executor;
}
else
{
loc = Location(executor);
}
open_exit(executor, loc, direction, dest);
// Open the back link if we can.
//
if (nlinks >= 2)
{
const dbref destnum = parse_linkable_room(executor, dest);
// #1188: HOME is a valid *link* destination, but not a real room to
// open an exit from. open_exit() would bail on !Good_obj with no
// message, leaving a silent one-way open.
//
if (destnum == HOME)
{
notify_quiet(executor,
T("You cant open a reverse exit at home."));
}
else if (Good_obj(destnum))
{
UTF8 buff[I32BUF_SIZE];
mux_ltoa(loc, buff);
open_exit(executor, destnum, links[1], buff);
}
}
}
// ---------------------------------------------------------------------------
// link_exit, do_link: Set destination(exits), dropto(rooms) or
// home(player,thing)
void link_exit(const dbref player, const dbref exit, const dbref dest)
{
// Make sure we can link there
//
if ( dest != HOME
&& !Link_Anywhere(player)
&& ( ( !Controls(player, dest)
&& !Link_ok(dest))
|| !could_doit(player, dest, A_LLINK)))
{
notify_quiet(player, NOPERM_MESSAGE);
return;
}
// Exit must be unlinked or controlled by you
//
if ( Location(exit) != NOTHING
&& !Controls(player, exit))
{
notify_quiet(player, NOPERM_MESSAGE);
return;
}
if (NoModify(exit) && !WizRoy(player))
{
notify_quiet(player, NOPERM_MESSAGE);
return;
}
// Handle costs
//
int cost = mudconf.linkcost;
int quot = 0;
if (Owner(exit) != Owner(player))
{
cost += mudconf.opencost;
quot += mudconf.exit_quota;
}
if (!canpayfees(player, player, cost, quot))
{
return;
}
// Pay the owner for his loss.
//
if (Owner(exit) != Owner(player))
{
giveto(Owner(exit), mudconf.opencost);
add_quota(Owner(exit), quot);
s_Owner(exit, Owner(player));
s_Flags(exit, FLAG_WORD1, (db[exit].fs.word[FLAG_WORD1] & ~(INHERIT | WIZARD)) | HALT);
}
// Link has been validated and paid for, do it and tell the player
//
s_Location(exit, dest);
if (!Quiet(player))
{
notify_quiet(player, M_("Linked."));
}
route_invalidate();
}
void do_link
(
const dbref executor,
const dbref caller,
dbref enactor,
const int eval,
const int key,
const int nargs,
UTF8 *what,
UTF8 *where,
const UTF8 *cargs[],
const int ncargs
)
{
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
// Find the thing to link
//
init_match(executor, what, TYPE_EXIT);
match_everything(0);
const dbref thing = noisy_match_result();
if (thing == NOTHING)
{
return;
}
// Allow unlink if where is not specified
//
if (!where || !*where)
{
2018-10-03 17:54:51 +00:00
do_unlink(executor, caller, enactor, 0, key, what, nullptr, 0);
return;
}
dbref room;
UTF8 *buff;
switch (Typeof(thing))
{
case TYPE_EXIT:
// Set destination
//
room = parse_linkable_room(executor, where);
if (Good_obj(room) || room == HOME)
{
link_exit(executor, thing, room);
}
break;
case TYPE_PLAYER:
case TYPE_THING:
// Set home.
//
if (!Controls(executor, thing))
{
notify_quiet(executor, NOPERM_MESSAGE);
break;
}
if (NoModify(thing) && !WizRoy(executor))
{
notify_quiet(executor, NOPERM_MESSAGE);
break;
}
init_match(executor, where, NOTYPE);
match_everything(MAT_NO_EXITS);
room = noisy_match_result();
if (!Good_obj(room))
{
break;
}
if (!Has_contents(room))
{
notify_quiet(executor, M_("Cant link to an exit."));
break;
}
if ( !can_set_home(executor, thing, room)
|| !could_doit(executor, room, A_LLINK))
{
notify_quiet(executor, NOPERM_MESSAGE);
}
else if (room == HOME)
{
notify_quiet(executor, M_("Cant set home to home."));
}
else
{
const dbref nHomeOrig = Home(thing);
const dbref nHomeNew = room;
s_Home(thing, nHomeNew);
if (!Quiet(executor))
{
nls: one msgid per sentence for the home/dropto change messages (#1419) Folded in from review. create.cpp built two sentences out of three M_() fragments each: "Home of %s(#%d) changed from " + "%s(#%d) to " + "%s(#%d)." Three catalogue entries for one sentence is worse than leaving it unmarked, because it looks translated while making the sentence permanently untranslatable: * clause order is frozen -- a translator cannot move "changed from" relative to the subject, and languages that invert from/to or put the verb last cannot express it at all; * the trailing spaces are load-bearing and invisible in a PO editor, so dropping one runs the sentence together with nothing to catch it; * "%s(#%d) to " is not a sentence in any language and gives the translator no context for what "to" joins; * "%s(#%d)." and "%s(#%d) to " were shared between the home and dropto sites, so one translation had to serve both. Whole sentences are the point of the Phase 3 slices -- #1568's own summary gives "makes whole sentences untranslatable as a unit" as the reason for allowing formats at all. Now one format each, six conversions, and the LBuf/safe_str assembly goes away with them. Output is unchanged; verified against a throwaway game: Home of Thing(#3) changed from Limbo(#0) to Limbo(#0). test-nls 570 msgids clean (down from 572: three fragments became one, twice, and two of the fragments were shared). test-format 31744 assertions pass. Catalogue rebuilt through `make -C mux/po mo`, which now carries msgfmt -c. Suite 1553 passed / 0 failed / 319 of 319. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 09:28:27 -06:00
// One msgid for one sentence. Assembling this from three
// M_() fragments made the clause order untranslatable: a
// translator cannot move "changed from" relative to the
// subject, the trailing spaces are load-bearing and invisible
// in a PO editor, and "%s(#%d) to " carries no context at all
// on its own. Whole-sentence formats are the point of the
// Phase 3 slices (#1419).
//
notify_quiet(executor, tprintf(
M_("Home of %s(#%d) changed from %s(#%d) to %s(#%d)."),
Name(thing), thing,
Name(nHomeOrig), nHomeOrig,
Name(nHomeNew), nHomeNew));
}
}
break;
case TYPE_ROOM:
// Set dropto.
//
if (!Controls(executor, thing))
{
notify_quiet(executor, NOPERM_MESSAGE);
break;
}
if (NoModify(thing) && !WizRoy(executor))
{
notify_quiet(executor, NOPERM_MESSAGE);
break;
}
room = parse_linkable_room(executor, where);
if ( !Good_obj(room)
&& room != HOME)
{
break;
}
if ( room != HOME
&& !isRoom(room))
{
notify_quiet(executor, M_("That is not a room!"));
}
else if ( room != HOME
&& !Link_Anywhere(executor)
&& ( ( !Controls(executor, room)
&& !Link_ok(room))
|| !could_doit(executor, room, A_LLINK)))
{
notify_quiet(executor, NOPERM_MESSAGE);
}
else
{
const dbref nDroptoOrig = Dropto(thing);
const dbref nDroptoNew = room;
s_Dropto(thing, room);
if (!Quiet(executor))
{
nls: one msgid per sentence for the home/dropto change messages (#1419) Folded in from review. create.cpp built two sentences out of three M_() fragments each: "Home of %s(#%d) changed from " + "%s(#%d) to " + "%s(#%d)." Three catalogue entries for one sentence is worse than leaving it unmarked, because it looks translated while making the sentence permanently untranslatable: * clause order is frozen -- a translator cannot move "changed from" relative to the subject, and languages that invert from/to or put the verb last cannot express it at all; * the trailing spaces are load-bearing and invisible in a PO editor, so dropping one runs the sentence together with nothing to catch it; * "%s(#%d) to " is not a sentence in any language and gives the translator no context for what "to" joins; * "%s(#%d)." and "%s(#%d) to " were shared between the home and dropto sites, so one translation had to serve both. Whole sentences are the point of the Phase 3 slices -- #1568's own summary gives "makes whole sentences untranslatable as a unit" as the reason for allowing formats at all. Now one format each, six conversions, and the LBuf/safe_str assembly goes away with them. Output is unchanged; verified against a throwaway game: Home of Thing(#3) changed from Limbo(#0) to Limbo(#0). test-nls 570 msgids clean (down from 572: three fragments became one, twice, and two of the fragments were shared). test-format 31744 assertions pass. Catalogue rebuilt through `make -C mux/po mo`, which now carries msgfmt -c. Suite 1553 passed / 0 failed / 319 of 319. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 09:28:27 -06:00
// Same whole-sentence treatment as the home case above (#1419).
//
notify_quiet(executor, tprintf(
M_("Dropto of %s(#%d) changed from %s(#%d) to %s(#%d)."),
Name(thing), thing,
Name(nDroptoOrig), nDroptoOrig,
Name(nDroptoNew), nDroptoNew));
}
}
break;
case TYPE_GARBAGE:
notify_quiet(executor, NOPERM_MESSAGE);
break;
default:
STARTLOG(LOG_BUGS, "BUG", "OTYPE");
buff = alloc_mbuf("do_link.LOG.badtype");
mux_sprintf(buff, MBUF_SIZE, T("Strange object type: object #%d = %d"),
thing, Typeof(thing));
log_text(buff);
free_mbuf(buff);
ENDLOG;
}
}
// ---------------------------------------------------------------------------
// do_parent: Set an object's parent field.
//
void do_parent
(
const dbref executor,
const dbref caller,
dbref enactor,
const int eval,
const int key,
const int nargs,
UTF8 *tname,
UTF8 *pname,
const UTF8 *cargs[],
const int ncargs
)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(key);
UNUSED_PARAMETER(nargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
// Get victim.
//
init_match(executor, tname, NOTYPE);
match_everything(0);
const dbref thing = noisy_match_result();
if (!Good_obj(thing))
{
return;
}
// Make sure we can do it.
//
if ( Going(thing)
|| !Controls(executor, thing))
{
notify_quiet(executor, NOPERM_MESSAGE);
return;
}
if (NoModify(thing) && !WizRoy(executor))
{
notify_quiet(executor, NOPERM_MESSAGE);
return;
}
2007-09-28 07:15:37 -07:00
// Save the previous parent for @aparent handling.
//
dbref previous_parent = db[thing].parent;
// Find out what the new parent is.
//
2007-09-28 07:15:37 -07:00
dbref parent = NOTHING;
if ('\0' != pname[0])
{
init_match(executor, pname, Typeof(thing));
match_everything(0);
parent = noisy_match_result();
if (!Good_obj(parent))
{
return;
}
// Make sure we have rights to set parent.
//
if (!Parentable(executor, parent))
{
notify_quiet(executor, NOPERM_MESSAGE);
return;
}
// Verify no recursive reference
//
2007-09-28 07:15:37 -07:00
int lev;
int curr;
ITER_PARENTS(parent, curr, lev)
{
if (curr == thing)
{
notify_quiet(executor, M_("You cant have yourself as a parent!"));
return;
}
}
}
s_Parent(thing, parent);
2007-09-28 07:15:37 -07:00
// Now that parent is set, handle zero, one, or two @aparent notifications
// as necessary.
//
2007-09-28 07:15:37 -07:00
if (parent != previous_parent)
{
2026-03-29 03:28:10 -06:00
route_invalidate();
2007-09-28 07:15:37 -07:00
// Setup the appropriate stack arguments.
//
UTF8 child[SBUF_SIZE];
const UTF8 removal[] = "1";
const UTF8 addition[] = "0";
2007-09-28 07:15:37 -07:00
UTF8 original_executor[SBUF_SIZE];
const UTF8 *xargs[3];
const int xnargs = 3;
2007-09-28 07:15:37 -07:00
xargs[0] = child;
xargs[2] = original_executor;
2007-09-28 07:15:37 -07:00
dbref aowner;
int aflags;
2007-09-28 07:15:37 -07:00
// Notify previous parent of the removal.
//
if (Good_obj(previous_parent))
{
UTF8* action = atr_get("do_parent.529", previous_parent, A_APARENT,
&aowner, &aflags);
2018-10-03 17:54:51 +00:00
if ( nullptr != action
2007-09-28 07:15:37 -07:00
&& '\0' != action[0])
{
mux_sprintf(child, SBUF_SIZE, T("#%d"), thing);
mux_sprintf(original_executor, SBUF_SIZE, T("#%d"), executor);
2007-09-28 07:15:37 -07:00
xargs[1] = removal;
2018-10-03 17:54:51 +00:00
did_it(previous_parent, previous_parent, 0, nullptr, 0, nullptr,
2007-09-28 07:15:37 -07:00
A_APARENT, 0, xargs, xnargs);
}
free_lbuf(action);
}
2007-09-28 07:15:37 -07:00
// Notify new parent of addition.
//
if (Good_obj(parent))
{
UTF8* action = atr_get("do_parent.550", parent, A_APARENT,
&aowner, &aflags);
2018-10-03 17:54:51 +00:00
if ( nullptr != action
2007-09-28 07:15:37 -07:00
&& '\0' != action[0])
{
mux_sprintf(child, SBUF_SIZE, T("#%d"), thing);
mux_sprintf(original_executor, SBUF_SIZE, T("#%d"), executor);
2007-09-28 07:15:37 -07:00
xargs[1] = addition;
2018-10-03 17:54:51 +00:00
did_it(parent, parent, 0, nullptr, 0, nullptr, A_APARENT, 0,
2007-09-28 07:15:37 -07:00
xargs, xnargs);
}
free_lbuf(action);
}
}
2007-09-28 07:15:37 -07:00
if ( !Quiet(thing)
&& !Quiet(executor))
{
2007-09-28 07:15:37 -07:00
if (NOTHING == parent)
{
notify_quiet(executor, M_("Parent cleared."));
2007-09-28 07:15:37 -07:00
}
else
2007-09-28 07:15:37 -07:00
{
notify_quiet(executor, M_("Parent set."));
2007-09-28 07:15:37 -07:00
}
}
}
// ---------------------------------------------------------------------------
// do_dig: Create a new room.
//
void do_dig(const dbref executor, const dbref caller, const dbref enactor, const int eval, const int key,
UTF8 *name, UTF8 *args[], const int nargs, const UTF8 *cargs[], const int ncargs)
{
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
// we don't need to know player's location! hooray!
//
if (!name || !*name)
{
notify_quiet(executor, M_("Dig what?"));
return;
}
const dbref room = create_obj(executor, TYPE_ROOM, name, 0);
if (room == NOTHING)
{
return;
}
local_data_create(room);
2008-01-29 12:16:54 -08:00
ServerEventsSinkNode *p = g_pServerEventsSinkListHead;
2018-10-03 17:54:51 +00:00
while (nullptr != p)
{
p->pSink->data_create(room);
p = p->pNext;
}
2008-01-29 12:16:54 -08:00
notify(executor, tprintf(M_("%s created as room #%d."), name, room));
UTF8 *buff = alloc_sbuf("do_dig");
if ( nargs >= 1
&& args[0]
&& *args[0])
{
mux_ltoa(room, buff);
open_exit(executor, Location(executor), args[0], buff);
}
if ( nargs >= 2
&& args[1]
&& *args[1])
{
mux_ltoa(Location(executor), buff);
open_exit(executor, room, args[1], buff);
}
free_sbuf(buff);
if (key == DIG_TELEPORT)
{
(void)move_via_teleport(executor, room, enactor, 0);
}
}
// ---------------------------------------------------------------------------
// do_create: Make a new object.
//
void do_create
(
const dbref executor,
const dbref caller,
dbref enactor,
const int eval,
const int key,
const int nargs,
UTF8 *name,
UTF8 *coststr,
const UTF8 *cargs[],
const int ncargs
)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(key);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
int cost = 0;
if (!name || !*name)
{
notify_quiet(executor, M_("Create what?"));
return;
}
else if ( nargs == 2
fix(win32): migrate the remaining mux_atol callers to mux_atoi64 (#1373) Completes the sweep the issue called for. mux_atol returns long, which is 32-bit on LLP64, so every caller silently truncated on Windows. Two of those were real defects (the truthiness family and cf_size, fixed in the preceding commits); the rest were latent, waiting for a value large enough to matter. Rather than audit 290 sites for whether each can reach 2^31 today, use the 64-bit parser everywhere and remove the class. A dbref cannot overflow now, but nothing stops a later caller passing that same site a timestamp or a byte count. Pure 1:1 substitution: 285 lines changed, and every removed line contained mux_atol while every added line contains mux_atoi64. No control flow, no types, no behaviour beyond the wider parse. This is a NO-OP on LP64 -- long is already 64-bit on Linux and macOS, so the generated code there is unchanged. It only widens the parse on Windows. Narrowing destinations are unaffected either way: `int x = mux_atoi64(s)` truncates exactly as `int x = mux_atol(s)` did, on both models. Left alone: mux_atol itself in mathutil, its declaration, and three comments that name it. Callers that genuinely want 32-bit semantics can still ask for them; none appear to. Verified on Windows: full solution builds clean with no new warnings, smoke is 1418 passed / 16 failed / 0 crashes / 306 of 306 dispatched -- identical to before the sweep, with the same 16 build-configuration failures (exp3 module not loaded, hmac/digest behind UNIX_DIGEST). Spot checks after the change: the boolean family returns 1 for multiples of 2^32, cf_size round-trips 3000000000 and still reads -1 as unlimited, and arithmetic, string and list functions are unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 10:03:45 -06:00
&& (cost = mux_atoi64(coststr)) < 0)
{
notify_quiet(executor, M_("You cant create an object for less than nothing!"));
return;
}
const dbref thing = create_obj(executor, TYPE_THING, name, cost);
if (thing == NOTHING)
{
return;
}
move_via_generic(thing, executor, NOTHING, 0);
s_Home(thing, new_home(executor));
if (!Quiet(executor))
{
notify(executor, tprintf(M_("%s created as object #%d"), Name(thing), thing));
}
local_data_create(thing);
2008-01-29 12:16:54 -08:00
ServerEventsSinkNode *p = g_pServerEventsSinkListHead;
2018-10-03 17:54:51 +00:00
while (nullptr != p)
{
p->pSink->data_create(thing);
p = p->pNext;
}
}
// ---------------------------------------------------------------------------
// do_clone: Create a copy of an object.
//
void do_clone
(
const dbref executor,
const dbref caller,
dbref enactor,
const int eval,
int key,
int nargs,
UTF8 *name,
UTF8 *arg2,
const UTF8 *cargs[],
const int ncargs
)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
dbref loc;
int cost;
if ( (key & CLONE_INVENTORY)
|| !Has_location(executor))
{
loc = executor;
}
else
{
loc = Location(executor);
}
if (!Good_obj(loc))
{
return;
}
init_match(executor, name, NOTYPE);
match_everything(0);
dbref thing = noisy_match_result();
if ( NOTHING == thing
|| AMBIGUOUS == thing)
{
return;
}
// Let players clone things set VISUAL. It's easier than retyping
// in all that data.
//
if (!Examinable(executor, thing))
{
notify_quiet(executor, NOPERM_MESSAGE);
return;
}
if (isPlayer(thing))
{
notify_quiet(executor, M_("You cannot clone players!"));
return;
}
// You can only make a parent link to what you control.
//
if ( !Controls(executor, thing)
&& !Parent_ok(thing)
&& (key & CLONE_FROM_PARENT))
{
notify_quiet(executor,
tprintf(M_("You dont control %s, ignoring /parent."),
Name(thing)));
key &= ~CLONE_FROM_PARENT;
}
Fix command-side verb correctness bugs (@clone, @ps, whisper, @flag, @mark) Correctness sweep of the command-side verb handlers. Five confirmed bugs plus a help-text correction, verified by dual-lens review and code reading (the read-only smoke harness can't exercise these verbs directly): - @clone/cost on an exit bypassed the "must control current location" check (it lived only on the non-/cost path), letting a builder splice a cloned exit into a room they do not control. (#855) - The @mark/@mark_all/@apply_marked DB-cleaning refusal cited @unmark_all, which does not exist (produces "Huh?"); corrected to @mark_all/clear. (#856) - @ps <object> reported nothing for a controlled object owned by another player; do_ps was missing the non-player-target else clause that the sibling @halt has (clear the owner filter). (#857) - whisper "<quoted name>" skipped the locality/connected gate the unquoted form applies, giving a success confirmation plus a delivery error and polluting A_LASTWHISPER; also fixed an adjacent quoted-name continue that did not advance the parser. (#858) - @flag/remove of an unknown/empty flag name was silent; now reports an error like the other flag-name failure paths. (#859) - report help said 8-hour segments but the code uses 4 (deliberately, per 4a845139f); corrected the help. (#860) Also restores the "## JIT / DBT Engine" CHANGES heading dropped during an earlier 2.14.0.8 edit. Build clean, all 1264 smoke tests still pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 06:29:11 -06:00
// Cloning an exit splices it into your current location's exit list, so
// it requires control of that location -- regardless of /cost (with
// /inventory, loc is the executor, which it always controls). The check
// must run before the cost branches, not only on the non-/cost path.
//
if ( TYPE_EXIT == Typeof(thing)
&& !Controls(executor, loc))
{
notify_quiet(executor, NOPERM_MESSAGE);
return;
}
// Determine the cost of cloning
//
const dbref new_owner = (key & CLONE_PRESERVE) ? Owner(thing) : Owner(executor);
if (key & CLONE_SET_COST)
{
fix(win32): migrate the remaining mux_atol callers to mux_atoi64 (#1373) Completes the sweep the issue called for. mux_atol returns long, which is 32-bit on LLP64, so every caller silently truncated on Windows. Two of those were real defects (the truthiness family and cf_size, fixed in the preceding commits); the rest were latent, waiting for a value large enough to matter. Rather than audit 290 sites for whether each can reach 2^31 today, use the 64-bit parser everywhere and remove the class. A dbref cannot overflow now, but nothing stops a later caller passing that same site a timestamp or a byte count. Pure 1:1 substitution: 285 lines changed, and every removed line contained mux_atol while every added line contains mux_atoi64. No control flow, no types, no behaviour beyond the wider parse. This is a NO-OP on LP64 -- long is already 64-bit on Linux and macOS, so the generated code there is unchanged. It only widens the parse on Windows. Narrowing destinations are unaffected either way: `int x = mux_atoi64(s)` truncates exactly as `int x = mux_atol(s)` did, on both models. Left alone: mux_atol itself in mathutil, its declaration, and three comments that name it. Callers that genuinely want 32-bit semantics can still ask for them; none appear to. Verified on Windows: full solution builds clean with no new warnings, smoke is 1418 passed / 16 failed / 0 crashes / 306 of 306 dispatched -- identical to before the sweep, with the same 16 build-configuration failures (exp3 module not loaded, hmac/digest behind UNIX_DIGEST). Spot checks after the change: the boolean family returns 1 for multiples of 2^32, cf_size round-trips 3000000000 and still reads -1 as unlimited, and arithmetic, string and list functions are unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 10:03:45 -06:00
cost = mux_atoi64(arg2);
if (cost < mudconf.createmin)
cost = mudconf.createmin;
if (cost > mudconf.createmax)
cost = mudconf.createmax;
2018-10-03 17:54:51 +00:00
arg2 = nullptr;
}
else
{
cost = 1;
switch (Typeof(thing))
{
case TYPE_THING:
cost = OBJECT_DEPOSIT((mudconf.clone_copy_cost) ?
Pennies(thing) : 1);
break;
case TYPE_ROOM:
cost = mudconf.digcost;
break;
case TYPE_EXIT:
cost = mudconf.digcost;
break;
}
}
// Go make the clone object.
//
bool bValid = false;
size_t nValidName;
const UTF8 *pValidName = nullptr;
switch (Typeof(thing))
{
case TYPE_THING:
pValidName = MakeCanonicalObjectName(arg2, &nValidName, &bValid, mudconf.thing_name_charset);
break;
case TYPE_ROOM:
pValidName = MakeCanonicalObjectName(arg2, &nValidName, &bValid, mudconf.room_name_charset);
break;
case TYPE_EXIT:
pValidName = MakeCanonicalExitName(arg2, &nValidName, &bValid);
break;
}
const UTF8 *clone_name;
if (bValid)
{
clone_name = pValidName;
}
else
{
clone_name = Name(thing);
}
const dbref clone = create_obj(new_owner, Typeof(thing), clone_name, cost);
if (clone == NOTHING)
{
return;
}
// Copy in the new data.
//
if (key & CLONE_FROM_PARENT)
{
s_Parent(clone, thing);
}
else
{
atr_cpy(clone, thing, false);
}
// Reset the name, since we cleared the attributes.
//
s_Name(clone, clone_name);
// Reset the pennies, since it looks like we stamped on that, too.
//
s_Pennies(clone, OBJECT_ENDOWMENT(cost));
FLAGSET clearflags;
TranslateFlags_Clone(clearflags.word, executor, key);
2018-10-03 17:54:51 +00:00
SetClearFlags(clone, clearflags.word, nullptr);
// Tell creator about it
//
if (!Quiet(executor))
{
if (arg2 && *arg2)
{
notify(executor,
tprintf(M_("%s cloned as %s, new copy is object #%d."),
Name(thing), arg2, clone));
}
else
{
notify(executor,
tprintf(M_("%s cloned, new copy is object #%d."),
Name(thing), clone));
}
}
// Put the new thing in its new home. Break any dropto or link, then
// try to re-establish it.
//
switch (Typeof(thing))
{
case TYPE_THING:
s_Home(clone, clone_home(executor, thing));
move_via_generic(clone, loc, executor, 0);
break;
case TYPE_ROOM:
s_Dropto(clone, NOTHING);
if (Dropto(thing) != NOTHING)
{
// #1181: @clone/preserve must not re-link via link_exit —
// that path charges open cost and steals ownership to the
// executor (wizard), clearing INHERIT|WIZARD and setting HALT.
//
if (key & CLONE_PRESERVE)
{
s_Dropto(clone, Dropto(thing));
route_invalidate();
}
else
{
link_exit(executor, clone, Dropto(thing));
}
}
break;
case TYPE_EXIT:
s_Exits(loc, insert_first(Exits(loc), clone));
s_Exits(clone, loc);
s_Location(clone, NOTHING);
if (Location(thing) != NOTHING)
{
// #1181: same preserve ownership rule as rooms/dropto above.
//
if (key & CLONE_PRESERVE)
{
s_Location(clone, Location(thing));
route_invalidate();
}
else
{
link_exit(executor, clone, Location(thing));
}
}
break;
}
// If same owner run ACLONE, else halt it. Also copy parent if we can.
//
if (new_owner == Owner(thing))
{
if (!(key & CLONE_FROM_PARENT))
{
s_Parent(clone, Parent(thing));
}
2018-10-03 17:54:51 +00:00
did_it(executor, clone, 0, nullptr, 0, nullptr, A_ACLONE, 0, nullptr, 0);
}
else
{
if ( !(key & CLONE_FROM_PARENT)
&& (Controls(executor, thing) || Parent_ok(thing)))
{
s_Parent(clone, Parent(thing));
}
s_Halted(clone);
}
local_data_clone(clone, thing);
2008-01-29 12:16:54 -08:00
ServerEventsSinkNode *p = g_pServerEventsSinkListHead;
2018-10-03 17:54:51 +00:00
while (nullptr != p)
{
p->pSink->data_clone(clone, thing);
p = p->pNext;
}
}
// ---------------------------------------------------------------------------
// do_pcreate: Create new players and robots.
//
void do_pcreate
(
const dbref executor,
const dbref caller,
const dbref enactor,
const int eval,
const int key,
const int nargs,
UTF8 *name,
UTF8 *pass,
const UTF8 *cargs[],
int ncargs
)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
const UTF8 *pmsg;
const bool isrobot = (key == PCRE_ROBOT);
const dbref newplayer = create_player(name, pass, executor, isrobot, &pmsg);
if (newplayer == NOTHING)
{
notify_quiet(executor, tprintf(M_("Failure creating %s. %s"), name, pmsg));
return;
}
AddToPublicChannel(newplayer);
AddToPlayerChannels(newplayer);
if (isrobot)
{
move_object(newplayer, Location(executor));
notify_quiet(executor,
tprintf(M_("New robot %s (#%d) created with password %s"),
name, newplayer, pass));
notify_quiet(executor, M_("Your robot has arrived."));
STARTLOG(LOG_PCREATES, "CRE", "ROBOT");
log_name(newplayer);
log_text(T(" created by "));
log_name(executor);
ENDLOG;
}
else
{
move_object(newplayer, mudconf.start_room);
notify_quiet(executor,
tprintf(M_("New player %s (#%d) created with password %s"),
name, newplayer, pass));
STARTLOG(LOG_PCREATES | LOG_WIZARD, "WIZ", "PCREA");
log_name(newplayer);
log_text(T(" created by "));
log_name(executor);
ENDLOG;
}
}
// ---------------------------------------------------------------------------
// destroyable: Indicates if target of a @destroy is a 'special' object in
// the database.
//
static bool destroyable(dbref victim)
{
if ( (victim == mudconf.default_home)
|| (victim == mudconf.start_home)
|| (victim == mudconf.start_room)
|| (victim == mudconf.master_room)
|| (victim == static_cast<dbref>(0))
|| (God(victim)))
{
return false;
}
return true;
}
// ---------------------------------------------------------------------------
// can_destroy_player, do_destroy: Destroy things.
//
static bool can_destroy_player(dbref player, dbref victim)
{
if (!Wizard(player))
{
notify_quiet(player, M_("Sorry, no suicide allowed."));
return false;
}
if (RealWizard(victim))
{
notify_quiet(player, M_("Even you cant do that!"));
return false;
}
return true;
}
static void ProcessMasterRoomADestroy(const dbref destroyer, const dbref thing)
{
2007-09-16 14:36:47 -07:00
const UTF8 *xargs[2];
2007-09-16 14:36:47 -07:00
switch (Typeof(thing))
{
case TYPE_ROOM:
2007-09-16 14:36:47 -07:00
xargs[1] = T("ROOM");
break;
2007-09-16 14:36:47 -07:00
case TYPE_EXIT:
2007-09-16 14:36:47 -07:00
xargs[1] = T("EXIT");
break;
2007-09-16 14:36:47 -07:00
case TYPE_PLAYER:
2007-09-16 14:36:47 -07:00
xargs[1] = T("PLAYER");
break;
2007-09-16 14:36:47 -07:00
case TYPE_THING:
2007-09-16 14:36:47 -07:00
xargs[1] = T("THING");
break;
2007-09-16 14:36:47 -07:00
default:
xargs[1] = S_("#-1");
2007-09-16 14:36:47 -07:00
break;
}
dbref master_room_obj = -1;
DOLIST(master_room_obj, Contents(mudconf.master_room))
{
2007-09-16 14:36:47 -07:00
if (thing == master_room_obj)
{
break;
}
2007-09-16 14:36:47 -07:00
if (Controls(master_room_obj, thing))
{
int aowner, aflags;
2007-09-16 14:36:47 -07:00
UTF8* act = atr_pget(master_room_obj, A_ADESTROY, &aowner, &aflags);
if ('\0' != act[0])
{
const int nxargs = 2;
2007-09-16 14:36:47 -07:00
CLinearTimeAbsolute lta;
UTF8 buf[SBUF_SIZE];
mux_sprintf(buf, SBUF_SIZE, T("#%d"), thing);
2007-09-16 14:36:47 -07:00
xargs[0] = buf;
wait_que(master_room_obj, destroyer, destroyer,
AttrTrace(aflags, 0), false, lta, NOTHING, 0, act, nxargs,
(const UTF8 **) xargs, mudstate.global_regs);
}
free_lbuf(act);
}
}
}
void do_destroy(const dbref executor, const dbref caller, dbref enactor, const int eval, int key, UTF8 *what, const UTF8 *cargs[], const int ncargs)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
// You can destroy anything you control.
//
dbref thing = match_controlled_quiet(executor, what);
// If you own a location, you can destroy its exits.
//
if ( thing == NOTHING
&& Controls(executor, Location(executor)))
{
init_match(executor, what, TYPE_EXIT);
match_exit();
thing = last_match_result();
}
// You may destroy DESTROY_OK things in your inventory.
//
if (thing == NOTHING)
{
init_match(executor, what, TYPE_THING);
match_possession();
thing = last_match_result();
if ( thing != NOTHING
&& !(isThing(thing) && Destroy_ok(thing)))
{
thing = NOPERM;
}
}
// Return an error if we didn't find anything to destroy.
//
if (match_status(executor, thing) == NOTHING)
{
return;
}
// Check SAFE and DESTROY_OK flags.
//
if ( Safe(thing, executor)
&& !(key & DEST_OVERRIDE)
&& !(isThing(thing) && Destroy_ok(thing)))
{
notify_quiet(executor, M_("Sorry, that object is protected. Use @destroy/override to destroy it."));
return;
}
// Check INDESTRUCTIBLE flag -- cannot be bypassed, even by Wizards.
//
if (Indestructible(thing))
{
notify_quiet(executor, M_("That object is indestructible."));
return;
}
// Make sure we're not trying to destroy a special object.
//
if (!destroyable(thing))
{
notify_quiet(executor, M_("You cant destroy that!"));
return;
}
// Make sure we can do it, on a type-specific basis.
//
2007-09-16 14:36:47 -07:00
if ( isPlayer(thing)
&& !can_destroy_player(executor, thing))
{
return;
}
size_t nCased;
UTF8 *pCased = mux_strlwr(object_types[Typeof(thing)].name, nCased);
if (Going(thing))
{
if (!mudconf.destroy_going_now)
{
notify_quiet(executor, tprintf(M_("No sense beating a dead %s."), pCased));
return;
}
key |= DEST_INSTANT;
}
// Check whether we should perform instant destruction.
//
const dbref ThingOwner = Owner(thing);
bool bInstant = (key & DEST_INSTANT) || Destroy_ok(thing) || Destroy_ok(ThingOwner);
if (!bInstant)
{
// Pre-destruction 'crumble' emits and one last possible showstopper.
//
switch (Typeof(thing))
{
case TYPE_ROOM:
notify_all(thing, executor, M_("The room shakes and begins to crumble."));
break;
case TYPE_PLAYER:
atr_add_raw(thing, A_DESTROYER, mux_ltoa_t(executor));
if (!atr_get_raw(thing, A_DESTROYER))
{
// Not a likely situation, but the player has too many
// attributes to remember it's destroyer, so we we need to
// take care of this more immediately.
//
bInstant = true;
notify(executor, M_("Player has a lot of attributes. Performing destruction immediately."));
break;
}
// FALL THROUGH
case TYPE_EXIT:
case TYPE_THING:
notify(executor, tprintf(M_("The %s shakes and begins to crumble."),
pCased));
break;
default:
notify(executor, M_("Weird object type cannot be destroyed."));
return;
}
if ( !bInstant
&& !Quiet(thing)
&& !Quiet(ThingOwner))
{
notify_quiet(ThingOwner,
tprintf(M_("You will be rewarded shortly for %s(#%d)."),
Moniker(thing), thing));
}
}
// Imperative Destruction emits.
//
if (!Quiet(executor))
{
if (Good_owner(ThingOwner))
{
if (ThingOwner == Owner(executor))
{
if (!Quiet(thing))
{
notify(executor, tprintf(M_("Destroyed %s(#%d)."),
Moniker(thing), thing));
}
}
else if (ThingOwner == thing)
{
notify(executor, tprintf(M_("Destroyed %s(#%d)."),
Moniker(thing), thing));
}
else
{
LBuf tname = LBuf_Src("destroy_obj");
mux_strncpy(tname, Moniker(ThingOwner), LBUF_SIZE-1);
notify(executor, tprintf(M_("Destroyed %ss %s(#%d)."),
tname.get(), Moniker(thing), thing));
}
}
}
ProcessMasterRoomADestroy(executor, thing);
if (bInstant)
{
// Instant destruction by type.
//
switch (Typeof(thing))
{
case TYPE_EXIT:
destroy_exit(thing);
break;
case TYPE_PLAYER:
destroy_player(executor, thing);
break;
case TYPE_ROOM:
empty_obj(thing);
destroy_obj(thing);
break;
case TYPE_THING:
destroy_thing(thing);
break;
default:
notify(executor, M_("Weird object type cannot be destroyed."));
return;
}
}
s_Going(thing);
route_invalidate();
}