Byte-Quota Utilities (#330)

(an instance of Generic Utilities Package made by Quota)

     Verbs a user might want to call from a program:
      :bi_create -- built-in create() call, takes same args.
     
      :get_quota(who) -- just get the raw size_quota property
      :display_quota(who) -- prints to player the quota of who. If caller_perms() controls who, include any secondary characters. Called by @quota.
      :get_size_quota(who [allchars]) -- return the quota of who, if allchars flag set, add info from all secondary chars, if caller_perms() permits.
     
      :value_bytes(value) -- computes the size of the value.
      :object_bytes(object) -- computes the size of the object and caches it.
      :recent_object_bytes(object, days) -- computes and caches the size of object only if cached value more than days old. Returns cached value.
      :do_summary(user) -- prints out the results of summarize-one-user.
      :summarize_one_user(user) -- summarizes and caches space usage for user. See verb help for details.
     
     Verbs the system calls:
      :"creation_permitted verb_addition_permitted property_addition_permitted"(who) -- returns true if who is permitted to build.
      :initialize_quota(who) -- sets quota for newly created players
      :adjust_quota_for_programmer(who) -- empty; might add more quota to newly @progged player.
      :enable_create(who) -- sets .ownership_quota to 1
      :disable_create(who) -- sets .ownership_quota back to -1000 to prohibit create()
      :charge_quota(who, object) -- subtract the size of object from who's quota. Manipulates the #-unmeasured if what is not currently measured. Called by $wiz_utils:set_owner.
      :reimburse_quota(who, object) -- add the size of object to who's quota. Ditto.
      :preliminary_reimburse_quota(who, object) -- Because the set_owner is done *after* an object has been turned into $garbage, ordinary reimbursement fails. So we use this verb in the $recycler.
      :set_quota(who, howmuch)
      :quota_remaining(who)
      :display_quota_summary -- internal, called by display quota
     
     See help @measure and help @quota for the command line verbs.
     
     
     Porter's notes: If you are planning on porting this system to another MOO, here are the things to grab in addition to @dumping all of $quota_utils:
     
     The following verbs have been changed on $prog:
     @prop*erty @verb @copy (@add-alias @copy-move as well)
     
     The following verbs have been changed on $wiz:
     @programmer @quota
     
     The following verbs have been changed on $wiz_utils:
     set_programmer set_owner make_player
     
     The following verbs have been changed on $builder:
     @quota _create
     
     This verb probably should have gone on $builder.
     @measure
     
     The followig verbs have been changed on $recycler
     _recycle _create setup_toad
     
     The following verb has been changed on $login:
     create
     
     And don't forget $alt_quota_utils, which has the object based implementation.

Go to location of this object, Quota.



VERB SOURCE CODE:

initialize_quota:
if (!caller_perms().wizard)
    return E_PERM;
else
    args[1].size_quota = this.default_quota;
    args[1].ownership_quota = this.large_negative_number;
endif
.


init_for_core:
if (!caller_perms().wizard)
    return E_PERM;
else
    $alt_quota_utils = this;
    set_verb_code(this, "can_peek", {"return $perm_utils:controls(args[1], args[2]);"});
    set_verb_code(this, "can_touch", {"return args[1].wizard;"});
    this.exempted = {};
endif
.


adjust_quota_for_programmer:
return 0;
.


enable_create:
if ((caller != this) && (!caller_perms().wizard))
    return E_PERM;
else
    args[1].ownership_quota = 1;
endif
.


disable_create:
if ((caller != this) && (!caller_perms().wizard))
    return E_PERM;
else
    args[1].ownership_quota = this.large_negative_number;
endif
.


parse_create_args:
"This figures out who is gonna own the stuff @create does.  If one arg, return caller_perms(). 
 If two args, then if caller_perms().wizard, args[2].";
if (length(args) == 1)
    return caller_perms();
elseif ((length(args) == 2) && $perm_utils:controls(caller_perms(), args[2]))
    return args[2];
else
    return E_INVARG;
endif
.


creation_permitted verb_addition_permitted property_addition_permitted:
"Here's the tricky one.  Collect all the user's characters' cached usage data and 
total quotas.  Compare same.  If usage bigger than quotas, return 0.  Else, add up 
the total number of objects that haven't been measured recently.  If greater than 
the allowed, return 0.  Else, reluctantly, return 1.";
who = args[1];
allwho = this:all_characters(who);
quota = 0;
usage = 0;
unmeasured = 0;
for x in (allwho)
    quota = quota + x.size_quota[1];
    usage = usage + x.size_quota[2];
    unmeasured = unmeasured + x.size_quota[4];
endfor
if (usage >= quota)
    return 0;
elseif (unmeasured >= this.max_unmeasured)
    return 0;
else
    return 1;
endif
.


all_characters:
if ((caller != this) && (!this:can_peek(caller_perms(), args[1])))
    return E_PERM;
    "elseif ($object_utils:has_property($local, \"registrar\"))";
elseif ($object_utils:has_property($local, "second_char_registry"))
    seconds = $local.second_char_registry:all_second_chars(args[1]);
    return seconds ? seconds | {args[1]};
else
    return {args[1]};
endif
.


display_quota:
who = args[1];
if (this:can_peek(caller_perms(), who) && (length(all = this:all_characters(who)) 
> 1))
    many = 1;
else
    many = 0;
    all = {who};
endif
if (many)
    tquota = 0;
    tusage = 0;
    ttime = $maxint;
    tunmeasured = 0;
    tunmeasurable = 0;
endif
for x in (all)
    quota = x.size_quota[1];
    usage = x.size_quota[2];
    timestamp = x.size_quota[3];
    unmeasured = x.size_quota[4];
    unmeasurable = 0;
    if (unmeasured >= 100)
        unmeasurable = unmeasured / 100;
        unmeasured = unmeasured % 100;
    endif
    if (many)
        player:tell(x.name, " quota: ", $string_utils:group_number(quota), "; usage: 
", $string_utils:group_number(usage), "; unmeasured: ", unmeasured, "; no .object_size: 
", unmeasurable, ".");
        tquota = tquota + quota;
        tusage = tusage + usage;
        ttime = min(ttime, timestamp);
        tunmeasured = tunmeasured + unmeasured;
        tunmeasurable = tunmeasurable + unmeasurable;
    endif
endfor
if (many)
    this:display_quota_summary(who, tquota, tusage, ttime, tunmeasured, tunmeasurable);
else
    this:display_quota_summary(who, quota, usage, timestamp, unmeasured, unmeasurable);
endif
.


get_quota:
return args[1].size_quota[1];
.


charge_quota:
"Charge args[1] for the quota required to own args[2]";
if ((caller == this) || caller_perms().wizard)
    usage_index = 2;
    unmeasured_index = 4;
    who = args[1];
    what = args[2];
    object_size = $object_utils:has_property(what, "object_size") ? what.object_size[1] 
| -1;
    if (object_size <= 0)
        who.size_quota[unmeasured_index] = who.size_quota[unmeasured_index] + 1;
    else
        who.size_quota[usage_index] = who.size_quota[usage_index] + object_size;
    endif
else
    return E_PERM;
endif
.


reimburse_quota:
"reimburse args[1] for the quota required to own args[2]";
"If it is a $garbage, then if who = $hacker, then we mostly ignore everything. Who 
cares what $hacker's quota looks like.";
if ((caller == this) || caller_perms().wizard)
    usage_index = 2;
    unmeasured_index = 4;
    who = args[1];
    what = args[2];
    if (parent(what) == $garbage)
        return 0;
    elseif (is_player(who))
        object_size = what.object_size[1];
        if (object_size <= 0)
            who.size_quota[unmeasured_index] = who.size_quota[unmeasured_index] - 
1;
        else
            who.size_quota[usage_index] = who.size_quota[usage_index] - object_size;
        endif
    endif
else
    return E_PERM;
endif
.


set_quota:
"Set args[1]'s quota to args[2]";
if (caller_perms().wizard || (caller == this))
    "Size_quota[1] is the total quota permitted.";
    return args[1].size_quota[1] = args[2];
else
    return E_PERM;
endif
.


get_size_quota:
"Return args[1]'s quotas.  second arg of 1 means add all second chars.";
who = args[1];
all = args[2];
if (all && ((caller == this) || this:can_peek(caller_perms(), who)))
    all = this:all_characters(who);
else
    all = {who};
endif
baseline = {0, 0, 0, 0};
for x in (all)
    baseline[1] = baseline[1] + x.size_quota[1];
    baseline[2] = baseline[2] + x.size_quota[2];
    baseline[3] = min(baseline[3], x.size_quota[3]) || x.size_quota[3];
    baseline[4] = baseline[4] + x.size_quota[4];
endfor
return baseline;
.


display_quota_summary:
who = args[1];
quota = args[2];
usage = args[3];
timestamp = args[4];
unmeasured = args[5];
unmeasurable = args[6];
player:tell(who.name, " has a total building quota of ", $string_utils:group_number(quota), 
" bytes.");
player:tell(who.ppc, " total usage was ", $string_utils:group_number(usage), " as 
of ", player:ctime(timestamp), ".");
if (usage > quota)
    player:tell(who.name, " is over quota by ", $string_utils:group_number(usage 
- quota), " bytes.");
else
    player:tell(who.name, " may create up to ", $string_utils:group_number(quota 
- usage), " more bytes of objects, properties, or verbs.");
endif
if (unmeasured)
    plural = unmeasured != 1;
    player:tell("There ", plural ? tostr("are ", unmeasured, " objects") | "is 1 
object", " which ", plural ? "are" | "is", " not yet included in the tally; this 
tally may thus be inaccurate.");
    if (unmeasured >= this.max_unmeasured)
        player:tell("The number of unmeasured objects is too large; no objects may 
be created until @measure new is used.");
    endif
endif
if (unmeasurable)
    plural = unmeasurable != 1;
    player:tell("There ", plural ? tostr("are ", unmeasurable, " objects") | "is 
1 object", " which do", plural ? "" | "es", " not have a .object_size property and 
will thus prevent additional building.", (who == player) ? "  Contact a wizard for 
assistance in having this situation repaired." | "");
endif
.


quota_remaining:
"This wants to only be called by a wizard cuz I'm lazy.  This is just for @second-char 
anyway.";
if (caller_perms().wizard)
    q = this:get_size_quota(args[1], 1);
    return q[1] - q[2];
endif
.


preliminary_reimburse_quota:
"This does the reimbursement work of the recycler, since we ignore $garbage in ordinary 
reimbursement.";
if (caller_perms().wizard)
    this:reimburse_quota(@args);
else
    return E_PERM;
endif
.


value_bytes:
set_task_perms(caller_perms());
v = args[1];
t = typeof(v);
if (t == LIST)
    b = ((length(v) + 1) * 2) * 4;
    for vv in (v)
        $command_utils:suspend_if_needed(2);
        b = b + this:value_bytes(vv);
    endfor
    return b;
elseif (t == STR)
    return length(v) + 1;
else
    return 0;
endif
.


object_bytes object_size:
"Algorithm:";
"  Base object takes up 13 words plus length of name.  (builtin props?)";
"  Each verb takes up 5 words overhead, plus length of its name, plus size of its 
code.";
"  Each property definition takes up 1 word plus length of property name, plus each 
property on the object takes up 4 words.  (Or, 5 per defined prop, 4 per inherited 
prop)";
"Note: each word is four bytes.";
foo = "delimit comments above from commented out code below";
"set_task_perms(caller_perms())";
o = args[1];
b = this:object_overhead_bytes(o);
vs = verbs(o);
b = b + this:verb_overhead_bytes(o);
for i in [0..length(vs) - 1]
    $command_utils:suspend_if_needed(5);
    vn = tostr(i);
    info = verb_info(o, vn);
    b = (b + length(info[3])) + 1;
    b = b + this:value_bytes(verb_code(o, vn));
endfor
ps = $object_utils:all_properties_suspended(o);
b = b + this:property_overhead_bytes(o, ps);
for p in (ps)
    if (!is_clear_property(o, p))
        $command_utils:suspend_if_needed(5);
        b = b + this:value_bytes(o.(p));
    endif
endfor
if ($object_utils:has_property(o, "object_size"))
    oldsize = o.object_size[1];
    if ($object_utils:has_property(o.owner, "size_quota"))
        "Update quota cache.";
        if (oldsize)
            o.owner.size_quota[2] = o.owner.size_quota[2] + (b - oldsize);
        else
            o.owner.size_quota[2] = o.owner.size_quota[2] + b;
            if (o.owner.size_quota[4] > 0)
                o.owner.size_quota[4] = o.owner.size_quota[4] - 1;
            endif
        endif
    endif
    o.object_size = {b, time()};
endif
return b;
.


do_summary:
who = args[1];
results = this:summarize_one_user(who);
total = results[1];
nuncounted = results[2];
nzeros = results[3];
oldest = results[4];
player:tell(who.name, " statistics:");
player:tell("  ", $string_utils:group_number(total), " bytes of storage measured.");
player:tell("  Oldest measurement date ", ctime(oldest), " (", $string_utils:from_seconds(time() 
- oldest), " ago)");
if (nzeros || nuncounted)
    player:tell("  Number of objects with no statistics recorded:  ");
    player:tell("      ", nzeros, " recently created, ", nuncounted, " not descendents 
of #1");
endif
.


summarize_one_user:
"Summarizes total space usage by one user (args[1]).  Optional second argument is 
a flag to say whether to re-measure all objects for this user; specify the number 
of seconds out of date you are willing to accept.  If negative, will only re-measure 
objects which have no recorded data.";
"Returns a list of four values:";
"  total : total measured space in bytes";
"  uncounted : Number of objects that were not counted because they aren't descendents 
of #1";
"  zeros : Number of objects which have been created too recently to have any measurement 
data at all (presumably none if re-measuring)";
"  most-out-of-date : the time() the oldest actual measurement was taken";
who = args[1];
if (length(args) == 2)
    if (args[2] < 0)
        earliest = 1;
    else
        earliest = time() - args[2];
    endif
else
    earliest = 0;
endif
nzeros = 0;
oldest = time();
nuncounted = 0;
ncounted = 1;
total = 0;
for x in ((typeof(who.owned_objects) == LIST) ? who.owned_objects | {})
    if ($object_utils:has_property(x, "object_size"))
        size = x.object_size[1];
        time = x.object_size[2];
        if (time < earliest)
            "Re-measure.  This side-effects x.object_size.";
            this:object_bytes(x);
            size = x.object_size[1];
            time = x.object_size[2];
        endif
        if (time)
            oldest = min(oldest, time);
        else
            nzeros = nzeros + 1;
        endif
        total = total + size;
        ncounted = ncounted + 1;
    else
        nuncounted = nuncounted + 1;
    endif
    $command_utils:suspend_if_needed(0);
endfor
"Cache the data...";
who.size_quota[2] = total;
who.size_quota[3] = oldest;
who.size_quota[4] = (nuncounted * this.unmeasured_multiplier) + nzeros;
return {total, nuncounted, nzeros, oldest};
.


recent_object_bytes:
":recent_object_bytes(x, n) -- return object size of x, guaranteed to be no more 
than n days old.  N defaults to this.cycle_days.";
object = args[1];
if (length(args) > 1)
    since = args[2];
else
    since = this.cycle_days;
endif
if (!valid(object))
    return 0;
elseif (object.object_size[2] > (time() - (((since * 24) * 60) * 60)))
    return object.object_size[1];
else
    return this:object_bytes(object);
endif
.


measurement_task:
if (!caller_perms().wizard)
    return E_PERM;
else
    num_processed = 0;
    num_repetitions = 0;
    usage_index = 2;
    time_index = 3;
    unmeasured_index = 4;
    players = setremove(players(), $hacker);
    lengthp = length(players);
    index = this.working in players;
    if (!index)
        "Uh, oh, our guy got reaped while we weren't looking.  Better look for someone 
else.";
        index = 1;
        while ((this.working > players[index]) && (index < lengthp))
            index = index + 1;
        endwhile
        this.working = players[index];
    endif
    day = (60 * 60) * 24;
    stop = time() + this.task_time_limit;
    early = time() - (day * this.cycle_days);
    while (time() < stop)
        who = players[index];
        if (is_player(who) && $object_utils:has_property(who, "size_quota"))
            "Robustness in the face of reaping...";
            usage = 0;
            unmeasured = 0;
            earliest = time();
            for o in (who.owned_objects)
                if ((o.owner == who) && (!(o in this.exempted)))
                    "sanity check: might have recycled while we suspended!";
                    if ($object_utils:has_property(o, "object_size"))
                        if (o.object_size[2] < early)
                            usage = usage + this:object_bytes(o);
                        else
                            usage = usage + o.object_size[1];
                            earliest = min(earliest, o.object_size[2]);
                        endif
                    else
                        unmeasured = unmeasured + 1;
                    endif
                endif
                $command_utils:suspend_if_needed(3);
            endfor
            if (!is_clear_property(who, "size_quota"))
                who.size_quota[usage_index] = usage;
                who.size_quota[unmeasured_index] = this.unmeasured_multiplier * unmeasured;
                who.size_quota[time_index] = earliest;
            else
                $mail_agent:send_message(player, player, "Quota Violation", {tostr(who, 
" has a clear .size_quota property."), $string_utils:names_of({who, @$object_utils:ancestors(who)})});
            endif
        elseif (is_player(who))
            "They don't have a size_quota property.  Whine.";
            $mail_agent:send_message(player, player, "Quota Violation", {tostr(who, 
" doesn't seem to have a .size_quota property."), $string_utils:names_of({who, @$object_utils:ancestors(who)})});
        endif
        if (index >= lengthp)
            index = 1;
        else
            index = index + 1;
        endif
        num_processed = num_processed + 1;
        if (num_processed > lengthp)
            "If we've gotten everyone up to threshold, try measuring some later than 
that.";
            early = early + ((24 * 60) * 60);
            num_repetitions = num_repetitions + 1;
            num_processed = 0;
        endif
        this.working = players[index];
    endwhile
    if ((!num_repetitions) && (num_processed < (lengthp / 2)))
        "Add this in because we aren't getting people summarized like we should. 
 We're going to work for way longer now, cuz we're going to do a second pass, but 
we really need to get those summaries done.  Only do this if we hardly did any work. 
 Note the -1 here: measure all newly created objects as well.  More work, sigh.";
        for x in (players)
            if (is_player(x))
                "Robustness as above...";
                this:summarize_one_user(x, -1);
            endif
        endfor
    endif
    $mail_agent:send_message(player, player, "quota-utils report", {tostr("About 
to measure objects of player ", this.working.name, " (", this.working, "), ", $string_utils:ordinal(this.working 
in players), " out of ", lengthp, ".  We processed ", num_processed + (lengthp * 
num_repetitions), " players in this run in ", num_repetitions, " times through all 
players.")});
    when = day - (time() % day);
    fork (((13 * 60) * 60) + when)
        this:measurement_task();
    endfork
endif
.


can_peek:
return (args[1] == this.owner) || $perm_utils:controls(args[1], args[2]);
.


can_touch:
return args[1].wizard || (args[1] in $local.arb.members);
.


do_breakdown:
dobj = args[1];
who = valid(caller_perms()) ? caller_perms() | player;
if (!this:can_peek(who, dobj.owner))
    return E_PERM;
endif
props = $object_utils:all_properties(dobj);
grand_total = obj_over = this:object_overhead_bytes(dobj);
output = {tostr("Object overhead:  ", obj_over)};
if (props)
    total = 0;
    lines = {};
    output = {@output, "Properties, defined and inherited, sorted by size:"};
    for x in (props)
        if (!is_clear_property(dobj, x))
            size = this:value_bytes(dobj.(x));
            total = total + size;
            if (size)
                lines = {@lines, {x, size}};
            endif
        endif
    endfor
    lines = $list_utils:reverse($list_utils:sort_suspended(0, lines, $list_utils:slice(lines, 
2)));
    for x in (lines)
        text = tostr("  ", x[1], ":  ", x[2]);
        output = {@output, text};
    endfor
    output = {@output, tostr("Total size of properties:  ", total)};
    grand_total = grand_total + total;
endif
prop_over = this:property_overhead_bytes(dobj, props);
output = {@output, tostr("Property overhead:  ", prop_over)};
grand_total = grand_total + prop_over;
if (verbs(dobj))
    output = {@output, "Verbs, sorted by size:"};
    total = 0;
    lines = {};
    for x in [0..length(verbs(dobj)) - 1]
        vname = verb_info(dobj, tostr(x))[3];
        size = this:value_bytes(verb_code(dobj, tostr(x)));
        total = total + size;
        lines = {@lines, {vname, size}};
    endfor
    lines = $list_utils:reverse($list_utils:sort_suspended(0, lines, $list_utils:slice(lines, 
2)));
    for x in (lines)
        text = tostr("  ", x[1], ":  ", x[2]);
        output = {@output, text};
    endfor
    output = {@output, tostr("Total size of verbs:  ", total)};
    grand_total = grand_total + total;
    verb_over = this:verb_overhead_bytes(dobj);
    output = {@output, tostr("Verb overhead:  ", verb_over)};
    grand_total = grand_total + verb_over;
endif
output = {@output, tostr("Grand total:  ", grand_total)};
return output;
.


object_overhead_bytes:
object = args[1];
return ((13 * 4) + length(object.name)) + 1;
.


property_overhead_bytes:
o = args[1];
if (length(args) > 1)
    ps = args[2];
else
    ps = $object_utils:all_properties_suspended(o);
endif
return (this:value_bytes(properties(o)) - 4) + ((length(ps) * 4) * 4);
.


verb_overhead_bytes:
o = args[1];
vs = verbs(o);
return (length(vs) * 5) * 4;
.


bi_create:
set_task_perms(caller_perms());
who = this:parse_create_args(@args);
if (this:creation_permitted(who))
    this:enable_create(who);
    value = create(@args);
    this:disable_create(who);
    if (typeof(value) != ERR)
        this:charge_quota(who, value);
    endif
    return value;
else
    return E_QUOTA;
endif
.



PROPERTY DATA:
      default_quota
      large_negative_number
      max_unmeasured
      unmeasured_multiplier
      working
      cycle_days
      task_time_limit
      byte_based
      exempted