tinymux/mux/modules/engine/walkdb.cpp
Stephen Dennis bc4cec252e fix(nls): plural morphology baked into msgids, and a fragment that could not be translated (#1717)
Found while translating mail (#1419).  The tell was a line wrong in English
before translation is even involved:

    MAIL: 1 messages in folder 0 [unnamed] (1 unread, 0 cleared).

MN_() exists for this (#1622) and #1655 converted the @mail/stats family;
this set was missed.  A translator cannot work around it -- the msgid has no
msgid_plural, so there is no second form for a catalogue to supply, and es
must pick one wording for all counts while ru and pl need three and can
express none of them.  The fix is in the C++, not the .po.

## Converted, 11 sites

mail.cpp, mail_mod.cpp   mailbox is full; URGENT MAIL urgent messages
walkdb.cpp               universe contains; objects @chowned; objects marked
object.cpp               objects @chowned to you
wiz.cpp                  You toaded ... objects @chowned
command.cpp              at most N players logged in

The mail module duplicates are converted alongside the engine's so the two
implementations do not drift (#1614).

## One was a different bug than the issue described

walkdb.cpp built its message as M_("%d objects %smarked") with %s = "" or
"un".  That is fragment assembly, not a missing plural: a translator handed
that %s cannot use it, because the prefix is English morphology rather than
a word.  Split into two whole sentences, each pluralised -- the #1575 /
#1588 rule.  Now renders "1 object marked" / "8 objects unmarked" instead of
a %s a catalogue has no way to fill.

## Two deliberately NOT converted

    MAIL: %d messages in folder %d [%s] (%d unread, %d cleared).
    %d objects = %d rooms, %d exits, %d things, %d players. (%d garbage)

MN_(s, p, n) selects one form from one n.  Four independent counts are four
independent plural decisions, and in Russian each noun needs its own form.
These want rewording, which is a design call rather than a mechanical
conversion, so they are left and recorded rather than half-fixed.

## Catalogue fallout

.pot 18 -> 27 plural entries; all three catalogues msgmerge'd.

xx is `complete` policy, so its nine new entries had to be filled and
unfuzzied or the guard fails -- which it duly did, first try.  msgmerge had
copied the old singular-only text into BOTH forms, so msgstr[0] said
"messages"; each form is now prefixed from its own msgid.

es and ko received real translations for all nine.

    es.po  190 -> 198/976  (20.3%)
    ko.po  229 -> 236/976  (24.2%)

## Verified end to end, at n=1 and n>1

    en   1 object marked      8 objects marked
    es   1 objeto marcado     8 objetos marcados
    ko   사물 1개를 표시했습니다   사물 8개를 표시했습니다
    xx   [xx] 1 object marked [xx] 8 objects marked

Korean renders identically for both, which is correct: it declares
nplurals=1, so the catalogue's own rule is being honoured rather than
English's imposed on it.

The unconverted multi-count line still reads "1 messages", as documented.

make test: Smoke 1561 x3, tests/nls 7, ko 4, plural 43.  TESTEXIT=0.

Refs #1419, #1575, #1588, #1614, #1622, #1655, #1702.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 21:31:32 +00:00

1551 lines
40 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*! \file walkdb.cpp
* \brief Support for commands that walk the entire db.
*
*/
#include "copyright.h"
#include "autoconf.h"
#include "config.h"
#include "externs.h"
#include "mux_table.h"
#include "sqlite_backend.h"
// Bind occurances of the universal var in ACTION to ARG, then run ACTION.
// Cmds run in low-prio Q after a 1 sec delay for the first one.
//
static void bind_and_queue(dbref executor, dbref caller, dbref enactor,
int eval, UTF8 *action, UTF8 *argstr,
const UTF8 *cargs[], int ncargs, int number)
{
CLinearTimeAbsolute lta;
wait_que(executor, caller, enactor, eval, false, lta, NOTHING, 0,
action,
ncargs, cargs,
mudstate.global_regs,
nullptr, // named_sargs
mudconf.safer_iter ? nullptr : argstr, // iter_token
number, // iter_number
nullptr); // switch_token
}
static void bind_and_process(dbref executor, dbref caller, dbref enactor,
int eval, UTF8 *action, UTF8 *argstr,
const UTF8 *cargs[], int ncargs, int number)
{
bool bLoopInBounds = ( 0 <= mudstate.in_loop
&& mudstate.in_loop < MAX_ITEXT);
if (bLoopInBounds)
{
mudstate.itext[mudstate.in_loop] = argstr;
mudstate.inum[mudstate.in_loop] = number;
}
mudstate.in_loop++;
// Execute the body as an action list: split on ';' and run each command
// inline, honoring @break/@assert and ;| piping (#788). parse_to()
// rewrites its buffer in place, so work on a private copy — do_dolist
// reuses the same 'action' buffer for every iteration.
//
LBuf tbuf = LBuf_Src("dolist.now");
mux_strncpy(tbuf, action, LBUF_SIZE - 1);
process_command_list_inline(executor, caller, enactor, eval, tbuf.get(),
cargs, ncargs);
mudstate.in_loop--;
if (bLoopInBounds)
{
mudstate.itext[mudstate.in_loop] = nullptr;
mudstate.inum[mudstate.in_loop] = 0;
}
}
// New @dolist. i.e.:
// @dolist #12 #34 #45 #123 #34644=@emit [name(##)]
//
// New switches added 12/92, /space (default) delimits list using spaces,
// and /delimit allows specification of a delimiter.
//
void do_dolist(dbref executor, dbref caller, dbref enactor, int eval, int key,
int nargs, UTF8 *list, UTF8 *command, const UTF8 *cargs[], int ncargs)
{
UNUSED_PARAMETER(nargs);
if (!list || *list == '\0')
{
notify(executor, M_("Thats terrific, but what should I do with the list?"));
return;
}
UTF8 *objstring, delimiter = ' ';
int number = 0;
UTF8 *curr = list;
if (key & DOLIST_DELIMIT)
{
UTF8 *tempstr = parse_to(&curr, ' ', EV_STRIP_CURLY);
if (1 < strlen(reinterpret_cast<char *>(tempstr)))
{
notify(executor, M_("The delimiter must be a single character!"));
return;
}
delimiter = *tempstr;
}
// Inline (/now) bodies run synchronously and may mutate the executor's
// own attributes, which can free the lbufs that 'list' and 'command' (the
// @dolist arguments) point into and corrupt our walk. Iterate over
// private copies so body execution cannot pull the rug out from under us.
// (The queued path is immune — wait_que() copies the body — but copying
// for both keeps the loop uniform and an lbuf alloc is a freelist pop.)
//
LBuf listcopy = LBuf_Src("dolist.list");
LBuf cmdcopy = LBuf_Src("dolist.cmd");
mux_strncpy(listcopy, curr, LBUF_SIZE - 1);
curr = listcopy.get();
if (nullptr != command)
{
mux_strncpy(cmdcopy, command, LBUF_SIZE - 1);
command = cmdcopy.get();
}
// For inline (/now) iteration, isolate @break/@assert to the loop: save
// and clear break_called so a break inside the body stops the dolist
// without leaking to the surrounding command list, then restore it.
//
bool save_break = break_called;
if (key & DOLIST_NOW)
{
break_called = false;
}
while (curr && *curr)
{
while (*curr == delimiter)
{
curr++;
}
if (*curr)
{
number++;
objstring = parse_to(&curr, delimiter, EV_STRIP_CURLY);
if (key & DOLIST_NOW)
{
bind_and_process(executor, caller, enactor, eval, command,
objstring, cargs, ncargs, number);
if (break_called)
{
// @break/@assert fired inside the body; stop the loop.
//
break;
}
}
else
{
bind_and_queue(executor, caller, enactor, eval, command,
objstring, cargs, ncargs, number);
}
}
}
if (key & DOLIST_NOW)
{
// Default: contain @break to the loop (it stopped the loop above
// but the enclosing action list continues). With /break, an
// inner @break also propagates outward and aborts the rest of
// the enclosing list, like @include's default (#788).
if ( (key & DOLIST_BREAK)
&& break_called)
{
// Leave break_called set.
}
else
{
break_called = save_break;
}
}
if (key & DOLIST_NOTIFY)
{
LBuf tbuf = LBuf_Src("dolist.notify_cmd");
mux_strncpy(tbuf, T("@notify/quiet me"), LBUF_SIZE-1);
CLinearTimeAbsolute lta;
wait_que(executor, caller, enactor, eval, false, lta, NOTHING,
A_SEMAPHORE,
tbuf,
ncargs, cargs,
mudstate.global_regs);
}
}
// Regular @find command
//
void do_find(dbref executor, dbref caller, dbref enactor, int eval, int key, UTF8 *name, const UTF8 *cargs[], int ncargs)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(key);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
UTF8 *buff;
if (!payfor(executor, mudconf.searchcost))
{
buff = tprintf(M_("You dont have enough %s."), mudconf.many_coins);
notify_quiet(executor, buff);
return;
}
dbref i, low_bound, high_bound;
parse_range(&name, &low_bound, &high_bound);
for (i = low_bound; i <= high_bound; i++)
{
if ( (Typeof(i) != TYPE_EXIT)
&& Controls(executor, i)
&& (!*name || string_match(PureName(i), name)))
{
buff = unparse_object(executor, i, false);
notify(executor, buff);
free_lbuf(buff);
}
}
notify(executor, M_("***End of List***"));
}
// ---------------------------------------------------------------------------
// get_stats, do_stats: Get counts of items in the db.
//
bool get_stats(dbref player, dbref who, STATS *info)
{
// Do we have permission?
//
if (Good_obj(who) && !Controls(player, who) && !Stat_Any(player))
{
notify(player, NOPERM_MESSAGE);
return false;
}
// Can we afford it?
//
if (!payfor(player, mudconf.searchcost))
{
notify(player, tprintf(M_("You dont have enough %s."), mudconf.many_coins));
return false;
}
info->s_total = 0;
info->s_rooms = 0;
info->s_exits = 0;
info->s_things = 0;
info->s_players = 0;
info->s_garbage = 0;
dbref i;
DO_WHOLE_DB(i)
{
if ((who == NOTHING) || (who == Owner(i)))
{
info->s_total++;
if (Going(i) && (Typeof(i) != TYPE_ROOM))
{
info->s_garbage++;
continue;
}
switch (Typeof(i))
{
case TYPE_ROOM:
info->s_rooms++;
break;
case TYPE_EXIT:
info->s_exits++;
break;
case TYPE_THING:
info->s_things++;
break;
case TYPE_PLAYER:
info->s_players++;
break;
default:
info->s_garbage++;
}
}
}
return true;
}
// Reworked by R'nice
//
void do_stats(dbref executor, dbref caller, dbref enactor, int eval, int key, UTF8 *name, const UTF8 *cargs[], int ncargs)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
dbref owner;
switch (key)
{
case STAT_ALL:
owner = NOTHING;
break;
case STAT_ME:
owner = Owner(executor);
break;
case STAT_PLAYER:
if (!(name && *name))
{
int nNextFree = mudstate.freelist;
if (mudstate.freelist == NOTHING)
{
nNextFree = mudstate.db_top;
}
notify(executor, tprintf(MN_(
"The universe contains %d object (next free is #%d).",
"The universe contains %d objects (next free is #%d).",
mudstate.db_top),
mudstate.db_top, nNextFree));
return;
}
owner = lookup_player(executor, name, true);
if (owner == NOTHING)
{
notify(executor, M_("Not found."));
return;
}
break;
default:
notify(executor, M_("Illegal combination of switches."));
return;
}
STATS statinfo;
if (!get_stats(executor, owner, &statinfo))
{
return;
}
notify(executor,
tprintf(M_("%d objects = %d rooms, %d exits, %d things, %d players. (%d garbage)"),
statinfo.s_total, statinfo.s_rooms, statinfo.s_exits,
statinfo.s_things, statinfo.s_players,
statinfo.s_garbage));
}
int chown_all(dbref from_player, dbref to_player, dbref acting_player, int key)
{
if (!isPlayer(from_player))
{
from_player = Owner(from_player);
}
if (!isPlayer(to_player))
{
to_player = Owner(to_player);
}
int count = 0;
if ( God(from_player)
&& !God(acting_player))
{
notify(acting_player, NOPERM_MESSAGE);
}
else
{
int i;
int quota_out = 0;
int quota_in = 0;
FLAGSET clearflags;
FLAGSET setflags;
bool bClearPowers;
TranslateFlags_Chown(clearflags.word, setflags.word, &bClearPowers, acting_player, key);
DO_WHOLE_DB(i)
{
if ( Owner(i) == from_player
&& Owner(i) != i)
{
switch (Typeof(i))
{
case TYPE_PLAYER:
s_Owner(i, i);
quota_out += mudconf.player_quota;
break;
case TYPE_THING:
s_Owner(i, to_player);
quota_out += mudconf.thing_quota;
quota_in -= mudconf.thing_quota;
break;
case TYPE_ROOM:
s_Owner(i, to_player);
quota_out += mudconf.room_quota;
quota_in -= mudconf.room_quota;
break;
case TYPE_EXIT:
s_Owner(i, to_player);
quota_out += mudconf.exit_quota;
quota_in -= mudconf.exit_quota;
break;
default:
s_Owner(i, to_player);
}
if (key & CHOWN_NOZONE)
{
s_Zone(i, NOTHING);
}
SetClearFlags(i, clearflags.word, setflags.word);
if (bClearPowers)
{
s_Powers(i, 0);
s_Powers2(i, 0);
}
// Always halt the queue.
//
halt_que(NOTHING, i);
count++;
}
}
add_quota(from_player, quota_out);
add_quota(to_player, quota_in);
}
return count;
}
void do_chownall
(
dbref executor,
dbref caller,
dbref enactor,
int eval,
int key,
int nargs,
UTF8 *from,
UTF8 *to,
const UTF8 *cargs[],
int ncargs
)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
init_match(executor, from, TYPE_PLAYER);
match_neighbor();
match_absolute();
match_player();
dbref victim = noisy_match_result();
if (NOTHING == victim)
{
return;
}
else if (!isPlayer(victim))
{
notify(executor, M_("Victim must be a player."));
return;
}
dbref recipient = executor;
if ( nullptr != to
&& to[0] != '\0')
{
init_match(executor, to, TYPE_PLAYER);
match_neighbor();
match_absolute();
match_player();
recipient = noisy_match_result();
if (NOTHING == recipient)
{
return;
}
}
int count = chown_all(victim, recipient, executor, key);
if (!Quiet(executor))
{
notify(executor, tprintf(MN_("%d object @chowned.",
"%d objects @chowned.", count), count));
}
}
#define ANY_OWNER (-2)
static void er_mark_disabled(dbref player)
{
notify(player,
M_("The mark commands are not allowed while DB cleaning is enabled."));
notify(player,
M_("Use the @disable cleaning command to disable automatic cleaning."));
notify(player,
M_("Remember to @mark_all/clear before re-enabling automatic cleaning."));
}
// ---------------------------------------------------------------------------
// do_search: Walk the db reporting various things (or setting/clearing mark
// bits)
//
bool search_setup(dbref player, UTF8 *searchfor, SEARCH *parm)
{
// Crack arg into <pname> <type>=<targ>,<low>,<high>
//
UTF8 *pname = parse_to(&searchfor, '=', EV_STRIP_TS);
if (!pname || !*pname)
{
pname = const_cast<UTF8 *>(T("me"));
}
else
{
size_t nCased;
pname = mux_strlwr(pname, nCased);
}
UTF8 *searchtype;
if (searchfor && *searchfor)
{
searchtype = reinterpret_cast<UTF8 *>(strrchr(reinterpret_cast<char *>(pname), ' '));
if (searchtype)
{
*searchtype++ = '\0';
}
else
{
searchtype = pname;
pname = const_cast<UTF8 *>(T(""));
}
}
else
{
searchtype = const_cast<UTF8 *>(T(""));
}
// If the player name is quoted, strip the quotes.
//
if (*pname == '"')
{
size_t k = strlen(reinterpret_cast<char *>(pname)) - 1;
if (pname[k] == '"')
{
pname[k] = '\0';
pname++;
}
}
// Strip any range arguments.
//
parse_range(&searchfor, &parm->low_bound, &parm->high_bound);
// Set limits on who we search.
//
parm->s_owner = Owner(player);
parm->s_wizard = Search(player);
parm->s_rst_owner = NOTHING;
if (!*pname)
{
parm->s_rst_owner = parm->s_wizard ? ANY_OWNER : player;
}
else if (pname[0] == '#')
{
parm->s_rst_owner = mux_atoi64(&pname[1]);
if (!Good_obj(parm->s_rst_owner))
{
parm->s_rst_owner = NOTHING;
}
else if (Typeof(parm->s_rst_owner) != TYPE_PLAYER)
{
parm->s_rst_owner = NOTHING;
}
}
else if (strcmp(reinterpret_cast<char *>(pname), "me") == 0)
{
parm->s_rst_owner = player;
}
else
{
parm->s_rst_owner = lookup_player(player, pname, true);
}
if (parm->s_rst_owner == NOTHING)
{
notify(player, tprintf(M_("%s: No such player"), pname));
return false;
}
// Set limits on what we search for.
//
int err = 0;
parm->s_rst_name = nullptr;
parm->s_rst_eval = nullptr;
parm->s_rst_type = NOTYPE;
parm->s_parent = NOTHING;
parm->s_zone = NOTHING;
for (int i = FLAG_WORD1; i <= FLAG_WORD3; i++)
{
parm->s_fset.word[i] = 0;
}
parm->s_pset.word1 = 0;
parm->s_pset.word2 = 0;
switch (searchtype[0])
{
case '\0':
// The no class requested class :)
//
break;
case 'e':
if (string_prefix(T("exits"), searchtype))
{
parm->s_rst_name = searchfor;
parm->s_rst_type = TYPE_EXIT;
}
else if (string_prefix(T("evaluate"), searchtype))
{
parm->s_rst_eval = searchfor;
}
else if (string_prefix(T("eplayer"), searchtype))
{
parm->s_rst_type = TYPE_PLAYER;
parm->s_rst_eval = searchfor;
}
else if (string_prefix(T("eroom"), searchtype))
{
parm->s_rst_type = TYPE_ROOM;
parm->s_rst_eval = searchfor;
}
else if (string_prefix(T("eobject"), searchtype))
{
parm->s_rst_type = TYPE_THING;
parm->s_rst_eval = searchfor;
}
else if (string_prefix(T("ething"), searchtype))
{
parm->s_rst_type = TYPE_THING;
parm->s_rst_eval = searchfor;
}
else if (string_prefix(T("eexit"), searchtype))
{
parm->s_rst_type = TYPE_EXIT;
parm->s_rst_eval = searchfor;
}
else
{
err = 1;
}
break;
case 'f':
if (string_prefix(T("flags"), searchtype))
{
// convert_flags ignores previous values of flag_mask and
// s_rst_type while setting them.
//
if ( !convert_flags( player, searchfor, &parm->s_fset,
&parm->s_rst_type) )
{
return false;
}
}
else
{
err = 1;
}
break;
case 'n':
if (string_prefix(T("name"), searchtype))
{
parm->s_rst_name = searchfor;
}
else
{
err = 1;
}
break;
case 'o':
if (string_prefix(T("objects"), searchtype))
{
parm->s_rst_name = searchfor;
parm->s_rst_type = TYPE_THING;
}
else
{
err = 1;
}
break;
case 'p':
if (string_prefix(T("players"), searchtype))
{
parm->s_rst_name = searchfor;
parm->s_rst_type = TYPE_PLAYER;
if (!*pname)
{
parm->s_rst_owner = ANY_OWNER;
}
}
else if (string_prefix(T("parent"), searchtype))
{
parm->s_parent = match_controlled(player, searchfor);
if (!Good_obj(parm->s_parent))
{
return false;
}
if (!*pname)
{
parm->s_rst_owner = ANY_OWNER;
}
}
else if (string_prefix(T("power"), searchtype))
{
if (!decode_power(player, searchfor, &parm->s_pset))
{
return false;
}
}
else
{
err = 1;
}
break;
case 'r':
if (string_prefix(T("rooms"), searchtype))
{
parm->s_rst_name = searchfor;
parm->s_rst_type = TYPE_ROOM;
}
else
{
err = 1;
}
break;
case 't':
if (string_prefix(T("type"), searchtype))
{
if (searchfor[0] == '\0')
{
break;
}
if (string_prefix(T("rooms"), searchfor))
{
parm->s_rst_type = TYPE_ROOM;
}
else if (string_prefix(T("exits"), searchfor))
{
parm->s_rst_type = TYPE_EXIT;
}
else if (string_prefix(T("objects"), searchfor))
{
parm->s_rst_type = TYPE_THING;
}
else if (string_prefix(T("things"), searchfor))
{
parm->s_rst_type = TYPE_THING;
}
else if (string_prefix(T("players"), searchfor))
{
parm->s_rst_type = TYPE_PLAYER;
if (!*pname)
{
parm->s_rst_owner = ANY_OWNER;
}
}
else
{
notify(player, tprintf(M_("%s: unknown type"), searchfor));
return false;
}
}
else if (string_prefix(T("things"), searchtype))
{
parm->s_rst_name = searchfor;
parm->s_rst_type = TYPE_THING;
}
else
{
err = 1;
}
break;
case 'z':
if (string_prefix(T("zone"), searchtype))
{
parm->s_zone = match_controlled(player, searchfor);
if (!Good_obj(parm->s_zone))
{
return false;
}
if (!*pname)
{
parm->s_rst_owner = ANY_OWNER;
}
}
else
{
err = 1;
}
break;
default:
err = 1;
}
if (err)
{
notify(player, tprintf(M_("%s: unknown class"), searchtype));
return false;
}
// Make sure player is authorized to do the search.
//
if ( !parm->s_wizard
&& (parm->s_rst_type != TYPE_PLAYER)
&& (parm->s_rst_owner != player)
&& (parm->s_rst_owner != ANY_OWNER))
{
notify(player, M_("You need a search warrant to do that!"));
return false;
}
// Make sure player has money to do the search.
//
if (!payfor(player, mudconf.searchcost))
{
notify(player,
tprintf(M_("You dont have enough %s to search. (You need %d)"),
mudconf.many_coins, mudconf.searchcost));
return false;
}
return true;
}
void search_perform(dbref executor, dbref caller, dbref enactor, SEARCH *parm)
{
POWER thing1powers, thing2powers;
UTF8 *bp;
UTF8 *buff = alloc_sbuf("search_perform.num");
int save_invk_ctr = mudstate.func_invk_ctr;
// SQLite fast path for simple searches.
//
// A search qualifies for the fast path when there is no name match,
// no eval expression, and no power filter — i.e., the criteria can be
// expressed purely in terms of object metadata columns. The SQL query
// uses indexes on owner, zone, or parent and avoids a full db[] scan.
//
if ( nullptr == parm->s_rst_name
&& nullptr == parm->s_rst_eval
&& 0 == parm->s_pset.word1
&& 0 == parm->s_pset.word2)
{
CSQLiteDB &sqldb = g_pSQLiteBackend->GetDB();
bool used_sql = false;
const bool has_flags = parm->s_fset.word[FLAG_WORD1] != 0
|| parm->s_fset.word[FLAG_WORD2] != 0
|| parm->s_fset.word[FLAG_WORD3] != 0;
const bool has_owner = parm->s_rst_owner != ANY_OWNER;
const bool has_type = parm->s_rst_type != NOTYPE;
const bool has_zone = parm->s_zone != NOTHING;
const bool has_parent = parm->s_parent != NOTHING;
// Case 1: Owner search (with optional type, no other filters).
//
if (has_owner && !has_flags && !has_zone && !has_parent)
{
if (sqldb.SearchByOwner(parm->s_rst_owner,
has_type ? static_cast<int>(parm->s_rst_type) : -1,
parm->low_bound, parm->high_bound,
[](dbref thing) { olist_add(thing); }))
{
used_sql = true;
}
}
// Case 2: Type-only search (no owner, no other filters).
//
else if (!has_owner && has_type && !has_flags && !has_zone && !has_parent)
{
if (sqldb.SearchByType(static_cast<int>(parm->s_rst_type),
parm->low_bound, parm->high_bound,
[](dbref thing) { olist_add(thing); }))
{
used_sql = true;
}
}
// Case 3: Zone search (no other filters).
//
else if (has_zone && !has_owner && !has_type && !has_flags && !has_parent)
{
if (sqldb.SearchByZone(parm->s_zone,
parm->low_bound, parm->high_bound,
[](dbref thing) { olist_add(thing); }))
{
used_sql = true;
}
}
// Case 4: Parent search (no other filters).
//
else if (has_parent && !has_owner && !has_type && !has_flags && !has_zone)
{
if (sqldb.SearchByParent(parm->s_parent,
parm->low_bound, parm->high_bound,
[](dbref thing) { olist_add(thing); }))
{
used_sql = true;
}
}
// Case 5: Flags-only search (no name, eval, power, zone, parent, owner).
//
else if (has_flags && !has_owner && !has_type && !has_zone && !has_parent)
{
if (sqldb.SearchByFlags(
parm->s_fset.word[FLAG_WORD1],
parm->s_fset.word[FLAG_WORD2],
parm->s_fset.word[FLAG_WORD3],
parm->low_bound, parm->high_bound,
[](dbref thing) { olist_add(thing); }))
{
used_sql = true;
}
}
if (used_sql)
{
free_sbuf(buff);
mudstate.func_invk_ctr = save_invk_ctr;
return;
}
}
// Full linear scan fallback for complex/combined searches.
//
dbref thing;
for (thing = parm->low_bound; thing <= parm->high_bound; thing++)
{
mudstate.func_invk_ctr = save_invk_ctr;
// Check for matching type.
//
if ( (parm->s_rst_type != NOTYPE)
&& (parm->s_rst_type != Typeof(thing)))
{
continue;
}
// Check for matching owner.
//
if ( (parm->s_rst_owner != ANY_OWNER)
&& (parm->s_rst_owner != Owner(thing)))
{
continue;
}
// Toss out destroyed things.
//
if (Going(thing))
{
continue;
}
// Check for matching parent.
//
if ( (parm->s_parent != NOTHING)
&& (parm->s_parent != Parent(thing)))
{
continue;
}
// Check for matching zone.
//
if ( (parm->s_zone != NOTHING)
&& (parm->s_zone != Zone(thing)))
{
continue;
}
// Check for matching flags.
//
bool b = false;
for (int i = FLAG_WORD1; i <= FLAG_WORD3; i++)
{
FLAG f = parm->s_fset.word[i];
if ((db[thing].fs.word[i] & f) != f)
{
b = true;
break;
}
}
if (b)
{
continue;
}
// Check for matching power.
//
thing1powers = Powers(thing);
thing2powers = Powers2(thing);
if ((thing1powers & parm->s_pset.word1) != parm->s_pset.word1)
{
continue;
}
if ((thing2powers & parm->s_pset.word2) != parm->s_pset.word2)
{
continue;
}
// Check for matching name.
//
if (parm->s_rst_name != nullptr)
{
if (!string_prefix(PureName(thing), parm->s_rst_name))
continue;
}
// Check for successful evaluation.
//
if (parm->s_rst_eval != nullptr)
{
buff[0] = '#';
mux_ltoa(thing, buff+1);
bool bLoopInBounds = ( 0 <= mudstate.in_loop
&& mudstate.in_loop < MAX_ITEXT);
if (bLoopInBounds)
{
mudstate.itext[mudstate.in_loop] = buff;
mudstate.inum[mudstate.in_loop] = 0;
}
mudstate.in_loop++;
LBuf result = LBuf_Src("search_perform");
bp = result.get();
mux_exec(parm->s_rst_eval, LBUF_SIZE-1, result, &bp, executor, caller, enactor,
EV_FCHECK | EV_EVAL | EV_NOTRACE, nullptr, 0);
*bp = '\0';
mudstate.in_loop--;
if (bLoopInBounds)
{
mudstate.itext[mudstate.in_loop] = nullptr;
mudstate.inum[mudstate.in_loop] = 0;
}
if (!*result || !xlate(result))
{
continue;
}
}
// It passed everything. Amazing.
//
olist_add(thing);
}
free_sbuf(buff);
mudstate.func_invk_ctr = save_invk_ctr;
}
static void search_mark(dbref player, int key)
{
dbref thing;
bool is_marked;
int nchanged = 0;
for (thing = olist_first(); thing != NOTHING; thing = olist_next())
{
is_marked = Marked(thing);
// Don't bother checking if marking and already marked (or if
// unmarking and not marked)
//
if ( ((key == SRCH_MARK) && is_marked)
|| ((key == SRCH_UNMARK) && !is_marked))
{
continue;
}
// Toggle the mark bit and update the counters.
//
if (key == SRCH_MARK)
{
Mark(thing);
nchanged++;
}
else
{
Unmark(thing);
nchanged++;
}
}
// Two whole sentences rather than "%smarked" with %s = "" / "un"
// (#1717). A translator handed that %s cannot use it -- the prefix is
// English morphology, not a word -- and the count needs a plural form on
// top. Same rule as #1575 / #1588: mark the sentence, never a piece.
//
if (SRCH_MARK == key)
{
notify(player, tprintf(MN_("%d object marked",
"%d objects marked", nchanged), nchanged));
}
else
{
notify(player, tprintf(MN_("%d object unmarked",
"%d objects unmarked", nchanged), nchanged));
}
return;
}
void do_search(dbref executor, dbref caller, dbref enactor, int eval, int key, UTF8 *arg, const UTF8 *cargs[], int ncargs)
{
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
UTF8 *buff, *bp;
dbref thing, from, to;
SEARCH searchparm;
if ((key != SRCH_SEARCH) && (mudconf.control_flags & CF_DBCHECK))
{
er_mark_disabled(executor);
return;
}
if (!search_setup(executor, arg, &searchparm))
{
return;
}
olist_push();
search_perform(executor, caller, enactor, &searchparm);
bool destitute = true;
bool flag;
// If we are doing a @mark command, handle that here.
//
if (key != SRCH_SEARCH)
{
search_mark(executor, key);
olist_pop();
return;
}
LBuf outbuf = LBuf_Src("do_search.outbuf");
int rcount = 0;
int ecount = 0;
int tcount = 0;
int pcount = 0;
// Room search.
//
if ( searchparm.s_rst_type == TYPE_ROOM
|| searchparm.s_rst_type == NOTYPE)
{
flag = true;
for (thing = olist_first(); thing != NOTHING; thing = olist_next())
{
if (Typeof(thing) != TYPE_ROOM)
{
continue;
}
if (flag)
{
flag = false;
destitute = false;
notify(executor, M_("\nROOMS:"));
}
buff = unparse_object(executor, thing, false);
notify(executor, buff);
free_lbuf(buff);
rcount++;
}
}
// Exit search.
//
if ( searchparm.s_rst_type == TYPE_EXIT
|| searchparm.s_rst_type == NOTYPE)
{
flag = true;
for (thing = olist_first(); thing != NOTHING; thing = olist_next())
{
if (Typeof(thing) != TYPE_EXIT)
{
continue;
}
if (flag)
{
flag = false;
destitute = false;
notify(executor, M_("\nEXITS:"));
}
from = Exits(thing);
to = Location(thing);
bp = outbuf;
buff = unparse_object(executor, thing, false);
safe_str(buff, outbuf, &bp);
free_lbuf(buff);
safe_str(T(" [from "), outbuf, &bp);
buff = unparse_object(executor, from, false);
safe_str(((from == NOTHING) ? T("NOWHERE") : buff), outbuf, &bp);
free_lbuf(buff);
safe_str(T(" to "), outbuf, &bp);
buff = unparse_object(executor, to, false);
safe_str(((to == NOTHING) ? T("NOWHERE") : buff), outbuf, &bp);
free_lbuf(buff);
safe_chr(']', outbuf, &bp);
*bp = '\0';
notify(executor, outbuf);
ecount++;
}
}
// Object search
//
if ( searchparm.s_rst_type == TYPE_THING
|| searchparm.s_rst_type == NOTYPE)
{
flag = true;
for (thing = olist_first(); thing != NOTHING; thing = olist_next())
{
if (Typeof(thing) != TYPE_THING)
{
continue;
}
if (flag)
{
flag = false;
destitute = false;
notify(executor, M_("\nOBJECTS:"));
}
bp = outbuf;
buff = unparse_object(executor, thing, false);
safe_str(buff, outbuf, &bp);
free_lbuf(buff);
safe_str(T(" [owner: "), outbuf, &bp);
buff = unparse_object(executor, Owner(thing), false);
safe_str(buff, outbuf, &bp);
free_lbuf(buff);
safe_chr(']', outbuf, &bp);
*bp = '\0';
notify(executor, outbuf);
tcount++;
}
}
// Player search
//
if ( searchparm.s_rst_type == TYPE_PLAYER
|| searchparm.s_rst_type == NOTYPE)
{
flag = true;
for (thing = olist_first(); thing != NOTHING; thing = olist_next())
{
if (Typeof(thing) != TYPE_PLAYER)
{
continue;
}
if (flag)
{
flag = false;
destitute = false;
notify(executor, M_("\nPLAYERS:"));
}
bp = outbuf;
buff = unparse_object(executor, thing, 0);
safe_str(buff, outbuf, &bp);
free_lbuf(buff);
if (searchparm.s_wizard)
{
safe_str(T(" [location: "), outbuf, &bp);
buff = unparse_object(executor, Location(thing), false);
safe_str(buff, outbuf, &bp);
free_lbuf(buff);
safe_chr(']', outbuf, &bp);
}
*bp = '\0';
notify(executor, outbuf);
pcount++;
}
}
// If nothing found matching search criteria.
//
if (destitute)
{
notify(executor, M_("Nothing found."));
}
else
{
mux_sprintf(outbuf, LBUF_SIZE,
M_("\nFound: Rooms...%d Exits...%d Objects...%d Players...%d"),
rcount, ecount, tcount, pcount);
notify(executor, outbuf);
}
olist_pop();
}
// ---------------------------------------------------------------------------
// do_markall: set or clear the mark bits of all objects in the db.
//
void do_markall(dbref executor, dbref caller, dbref enactor, int eval, int key)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
int i;
if (mudconf.control_flags & CF_DBCHECK)
{
er_mark_disabled(executor);
return;
}
if (key == MARK_SET)
{
Mark_all(i);
}
else if (key == MARK_CLEAR)
{
Unmark_all(i);
}
if (!Quiet(executor))
{
notify(executor, M_("Done."));
}
}
// ---------------------------------------------------------------------------
// do_apply_marked: Perform a command for each marked obj in the db.
//
void do_apply_marked( dbref executor, dbref caller, dbref enactor, int eval,
int key, UTF8 *command, const UTF8 *cargs[], int ncargs)
{
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(key);
if (mudconf.control_flags & CF_DBCHECK)
{
er_mark_disabled(executor);
return;
}
UTF8 *buff = alloc_sbuf("do_apply_marked");
int i;
int number = 0;
DO_WHOLE_DB(i)
{
if (Marked(i))
{
buff[0] = '#';
mux_ltoa(i, buff+1);
number++;
bind_and_queue(executor, caller, enactor, eval, command, buff,
cargs, ncargs, number);
}
}
free_sbuf(buff);
if (!Quiet(executor))
{
notify(executor, M_("Done."));
}
}
// ---------------------------------------------------------------------------
// Object list management routines: olist_push, olist_pop, olist_add,
// olist_first, olist_next
//
// olist_push: Create a new object list at the top of the object list stack.
//
void olist_push(void)
{
try
{
mudstate.olist.emplace();
}
catch (const std::bad_alloc &)
{
STARTLOG(LOG_PROBLEMS, "OBJ", "MEM");
log_printf(T("olist_push: out of memory."));
ENDLOG;
}
}
// olist_pop: Pop one entire list off the object list stack.
//
void olist_pop(void)
{
if (!mudstate.olist.empty())
{
mudstate.olist.pop();
}
}
// olist_add: Add an entry to the object list.
//
void olist_add(dbref item)
{
if (mudstate.olist.empty())
{
olist_push();
}
mudstate.olist.top().entries.push_back(item);
}
// olist_first: Return the first entry in the object list.
//
dbref olist_first(void)
{
if (mudstate.olist.empty())
{
return NOTHING;
}
auto &current = mudstate.olist.top();
if (current.entries.empty())
{
return NOTHING;
}
current.cursor = 0;
return current.entries[current.cursor++];
}
dbref olist_next(void)
{
if (mudstate.olist.empty())
{
return NOTHING;
}
auto &current = mudstate.olist.top();
if (current.cursor >= current.entries.size())
{
return NOTHING;
}
return current.entries[current.cursor++];
}
#define HOURS_PER_PERIOD 4
#define DAYS_PER_REPORT (2*7)
#define NPERIODS (24*DAYS_PER_REPORT/HOURS_PER_PERIOD)
void do_report(dbref executor, dbref caller, dbref enactor, int eval, int key)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(key);
UTF8 *buff = alloc_mbuf("do_report");
int nBin[NPERIODS];
int i;
for (i = 0; i < NPERIODS; i++)
{
nBin[i] = 0;
}
CLinearTimeAbsolute ltaNow, ltaPlayer;
ltaNow.GetLocal();
const int SecondsPerPeriod = HOURS_PER_PERIOD*60*60;
int iPlayer;
DO_WHOLE_DB(iPlayer)
{
if (isPlayer(iPlayer))
{
int aowner, aflags;
LBuf player_last = LBuf_Adopt(atr_get("do_report.43", iPlayer, A_LAST, &aowner, &aflags));
if (ltaPlayer.SetString(player_last))
{
CLinearTimeDelta ltd(ltaPlayer, ltaNow);
int ltdSeconds = ltd.ReturnSeconds();
int iBin = ltdSeconds / SecondsPerPeriod;
if (0 <= iBin && iBin < NPERIODS)
{
nBin[iBin]++;
}
}
}
}
int iHour, nSum = 0;
// Activity report (#1667 Phase 4 C8) — freeform numeric columns with
// separate header labels (no pre-spaced blob).
//
{
UTF8 header[MBUF_SIZE];
size_t pos = 0;
pos = mux_table_append_bytes(header, sizeof(header), pos,
reinterpret_cast<const char *>(M_("Day")));
pos = mux_table_append_bytes(header, sizeof(header), pos, " ");
pos = mux_table_append_bytes(header, sizeof(header), pos,
reinterpret_cast<const char *>(M_("Hours")));
pos = mux_table_append_bytes(header, sizeof(header), pos, " ");
pos = mux_table_append_bytes(header, sizeof(header), pos,
reinterpret_cast<const char *>(M_("Players")));
pos = mux_table_append_bytes(header, sizeof(header), pos, " ");
pos = mux_table_append_bytes(header, sizeof(header), pos,
reinterpret_cast<const char *>(M_("Total")));
notify(executor, header);
}
for (i = 0, iHour = 0; i < NPERIODS; i++, iHour += HOURS_PER_PERIOD)
{
nSum += nBin[i];
mux_sprintf(buff, MBUF_SIZE, T("%3d %03d - %03d: %6d %6d"),
iHour/24 + 1, iHour, iHour + HOURS_PER_PERIOD, nBin[i], nSum);
notify(executor, buff);
}
free_mbuf(buff);
}