/* ************************************************************************
*   File: shop.c                                        Part of CircleMUD *
*  Usage: shopkeepers: loading config files, spec procs.                  *
*                                                                         *
*  All rights reserved.  See license.doc for complete information.        *
*                                                                         *
*  Copyright (C) 1993, 94 by the Trustees of the Johns Hopkins University *
*  CircleMUD is based on DikuMUD, Copyright (C) 1990, 1991.               *
************************************************************************ */

/***
 * The entire shop rewrite for Circle 3.0 was done by Jeff Fink.  Thanks Jeff!
 ***/

#include "conf.h"
#include "sysdep.h"

#include "structs.h"
#include "comm.h"
#include "handler.h"
#include "db.h"
#include "interpreter.h"
#include "utils.h"
#include "shop.h"
#include "constants.h"

/* External variables */
extern struct time_info_data time_info;

/* Forward/External function declarations */
ACMD(do_tell);
ACMD(do_action);
ACMD(do_echo);
ACMD(do_say);
void sort_keeper_objs(struct char_data *keeper, int shop_nr);

/* Local variables */
struct shop_data *shop_index;
int top_shop = -1;
int cmd_say, cmd_tell, cmd_emote, cmd_slap, cmd_puke;

/* local functions */
char *read_shop_message(int mnum, room_vnum shr, FILE *shop_f, const char *why);
int read_type_list(FILE *shop_f, struct shop_buy_data *list, int new_format, int max);
int read_list(FILE *shop_f, struct shop_buy_data *list, int new_format, int max, int type);
void shopping_list(char *arg, struct char_data *ch, struct char_data *keeper, int shop_nr);
void shopping_value(char *arg, struct char_data *ch, struct char_data *keeper, int shop_nr);
void shopping_sell(char *arg, struct char_data *ch, struct char_data *keeper, int shop_nr);
struct obj_data *get_selling_obj(struct char_data *ch, char *name, struct char_data *keeper, int shop_nr, int msg);
struct obj_data *slide_obj(struct obj_data *obj, struct char_data *keeper, int shop_nr);
void shopping_buy(char *arg, struct char_data *ch, struct char_data *keeper, int shop_nr);
struct obj_data *get_purchase_obj(struct char_data *ch, char *arg, struct char_data *keeper, int shop_nr, int msg);
struct obj_data *get_hash_obj_vis(struct char_data *ch, char *name, struct obj_data *list);
struct obj_data *get_slide_obj_vis(struct char_data *ch, char *name, struct obj_data *list);
void boot_the_shops(FILE *shop_f, char *filename, int rec_count);
void assign_the_shopkeepers(void);
char *customer_string(int shop_nr, int detailed);
void list_all_shops(struct char_data *ch);
void list_detailed_shop(struct char_data *ch, int shop_nr);
void show_shops(struct char_data *ch, char *arg);
int is_ok_char(struct char_data *keeper, struct char_data *ch, int shop_nr);
int is_open(struct char_data *keeper, int shop_nr, int msg);
int is_ok(struct char_data *keeper, struct char_data *ch, int shop_nr);
void push(struct stack_data *stack, int pushval);
int top(struct stack_data *stack);
int pop(struct stack_data *stack);
void evaluate_operation(struct stack_data *ops, struct stack_data *vals);
int find_oper_num(char token);
int evaluate_expression(struct obj_data *obj, char *expr);
int trade_with(struct obj_data *item, int shop_nr);
int same_obj(struct obj_data *obj1, struct obj_data *obj2);
int shop_producing(struct obj_data *item, int shop_nr);
int transaction_amt(char *arg);
char *times_message(struct obj_data *obj, char *name, int num);
int buy_price(struct obj_data *obj, int shop_nr, struct char_data *keeper, struct char_data *buyer);
int sell_price(struct obj_data *obj, int shop_nr, struct char_data *keeper, struct char_data *seller);
char *list_object(struct obj_data *obj, int cnt, int oindex, int shop_nr, struct char_data *keeper, struct char_data *seller);
int ok_shop_room(int shop_nr, room_vnum room);
SPECIAL(shop_keeper);
int ok_damage_shopkeeper(struct char_data *ch, struct char_data *victim);
int add_to_list(struct shop_buy_data *list, int type, int *len, int *val);
int end_read_list(struct shop_buy_data *list, int len, int error);
void read_line(FILE *shop_f, const char *string, void *data);
void destroy_shops(void);


/* config arrays */
const char *operator_str[] = {
        "[({",
        "])}",
        "|+",
        "&*",
        "^'"
} ;

/* Constant list for printing out who we sell to */
const char *trade_letters[] = {
        "Good",                 /* First, the alignment based ones */
        "Evil",
        "Neutral",
        "Magic User",           /* Then the class based ones */
        "Cleric",
        "Thief",
        "Warrior",
        "\n"
};


const char *shop_bits[] = {
        "WILL_FIGHT",
        "USES_BANK",
        "\n"
};

int is_ok_char(struct char_data *keeper, struct char_data *ch, int shop_nr)
{
  char buf[MAX_INPUT_LENGTH];

  if (!CAN_SEE(keeper, ch)) {
    char actbuf[MAX_INPUT_LENGTH] = MSG_NO_SEE_CHAR;
    do_say(keeper, actbuf, cmd_say, 0);
    return (FALSE);
  }
  if (IS_GOD(ch))
    return (TRUE);

  if ((IS_GOOD(ch) && NOTRADE_GOOD(shop_nr)) ||
      (IS_EVIL(ch) && NOTRADE_EVIL(shop_nr)) ||
      (IS_NEUTRAL(ch) && NOTRADE_NEUTRAL(shop_nr))) {
    snprintf(buf, sizeof(buf), "%s %s", GET_NAME(ch), MSG_NO_SELL_ALIGN);
    do_tell(keeper, buf, cmd_tell, 0);
    return (FALSE);
  }
  if (IS_NPC(ch))
    return (TRUE);

  if ((IS_MAGIC_USER(ch) && NOTRADE_MAGIC_USER(shop_nr)) ||
      (IS_CLERIC(ch) && NOTRADE_CLERIC(shop_nr)) ||
      (IS_THIEF(ch) && NOTRADE_THIEF(shop_nr)) ||
      (IS_WARRIOR(ch) && NOTRADE_WARRIOR(shop_nr))) {
    snprintf(buf, sizeof(buf), "%s %s", GET_NAME(ch), MSG_NO_SELL_CLASS);
    do_tell(keeper, buf, cmd_tell, 0);
    return (FALSE);
  }
  return (TRUE);
}


int is_open(struct char_data *keeper, int shop_nr, int msg)
{
  char buf[MAX_INPUT_LENGTH];

  *buf = '\0';
  if (SHOP_OPEN1(shop_nr) > time_info.hours)
    strlcpy(buf, MSG_NOT_OPEN_YET, sizeof(buf));
  else if (SHOP_CLOSE1(shop_nr) < time_info.hours) {
    if (SHOP_OPEN2(shop_nr) > time_info.hours)
      strlcpy(buf, MSG_NOT_REOPEN_YET, sizeof(buf));
    else if (SHOP_CLOSE2(shop_nr) < time_info.hours)
      strlcpy(buf, MSG_CLOSED_FOR_DAY, sizeof(buf));
  }
  if (!*buf)
    return (TRUE);
  if (msg)
    do_say(keeper, buf, cmd_tell, 0);
  return (FALSE);
}


int is_ok(struct char_data *keeper, struct char_data *ch, int shop_nr)
{
  if (is_open(keeper, shop_nr, TRUE))
    return (is_ok_char(keeper, ch, shop_nr));
  else
    return (FALSE);
}


void push(struct stack_data *stack, int pushval)
{
  S_DATA(stack, S_LEN(stack)++) = pushval;
}


int top(struct stack_data *stack)
{
  if (S_LEN(stack) > 0)
    return (S_DATA(stack, S_LEN(stack) - 1));
  else
    return (NOTHING);
}


int pop(struct stack_data *stack)
{
  if (S_LEN(stack) > 0)
    return (S_DATA(stack, --S_LEN(stack)));
  else {
    log("SYSERR: Illegal expression %d in shop keyword list.", S_LEN(stack));
    return (0);
  }
}


void evaluate_operation(struct stack_data *ops, struct stack_data *vals)
{
  int oper;

  if ((oper = pop(ops)) == OPER_NOT)
    push(vals, !pop(vals));
  else {
    int val1 = pop(vals),
	val2 = pop(vals);

    /* Compiler would previously short-circuit these. */
    if (oper == OPER_AND)
      push(vals, val1 && val2);
    else if (oper == OPER_OR)
      push(vals, val1 || val2);
  }
}


int find_oper_num(char token)
{
  int oindex;

  for (oindex = 0; oindex <= MAX_OPER; oindex++)
    if (strchr(operator_str[oindex], token))
      return (oindex);
  return (NOTHING);
}


int evaluate_expression(struct obj_data *obj, char *expr)
{
  struct stack_data ops, vals;
  char *ptr, *end, name[MAX_STRING_LENGTH];
  int temp, eindex;

  if (!expr || !*expr)	/* Allows opening ( first. */
    return (TRUE);

  ops.len = vals.len = 0;
  ptr = expr;
  while (*ptr) {
    if (isspace(*ptr))
      ptr++;
    else {
      if ((temp = find_oper_num(*ptr)) == NOTHING) {
	end = ptr;
	while (*ptr && !isspace(*ptr) && find_oper_num(*ptr) == NOTHING)
	  ptr++;
	strncpy(name, end, ptr - end);	/* strncpy: OK (name/end:MAX_STRING_LENGTH) */
	name[ptr - end] = '\0';
	for (eindex = 0; *extra_bits[eindex] != '\n'; eindex++)
	  if (!str_cmp(name, extra_bits[eindex])) {
	    push(&vals, OBJ_FLAGGED(obj, 1 << eindex));
	    break;
	  }
	if (*extra_bits[eindex] == '\n')
	  push(&vals, isname(name, obj->name));
      } else {
	if (temp != OPER_OPEN_PAREN)
	  while (top(&ops) > temp)
	    evaluate_operation(&ops, &vals);

	if (temp == OPER_CLOSE_PAREN) {
	  if ((temp = pop(&ops)) != OPER_OPEN_PAREN) {
	    log("SYSERR: Illegal parenthesis in shop keyword expression.");
	    return (FALSE);
	  }
	} else
	  push(&ops, temp);
	ptr++;
      }
    }
  }
  while (top(&ops) != NOTHING)
    evaluate_operation(&ops, &vals);
  temp = pop(&vals);
  if (top(&vals) != NOTHING) {
    log("SYSERR: Extra operands left on shop keyword expression stack.");
    return (FALSE);
  }
  return (temp);
}


int trade_with(struct obj_data *item, int shop_nr)
{
  int counter;

  if (GET_OBJ_COST(item) < 1)
    return (OBJECT_NOVAL);

  if (OBJ_FLAGGED(item, ITEM_NOSELL))
    return (OBJECT_NOTOK);

  for (counter = 0; SHOP_BUYTYPE(shop_nr, counter) != NOTHING; counter++)
    if (SHOP_BUYTYPE(shop_nr, counter) == GET_OBJ_TYPE(item)) {
      if (GET_OBJ_VAL(item, 2) == 0 &&
		(GET_OBJ_TYPE(item) == ITEM_WAND ||
		 GET_OBJ_TYPE(item) == ITEM_STAFF))
	return (OBJECT_DEAD);
      else if (evaluate_expression(item, SHOP_BUYWORD(shop_nr, counter)))
	return (OBJECT_OK);
    }
  return (OBJECT_NOTOK);
}


int same_obj(struct obj_data *obj1, struct obj_data *obj2)
{
  int aindex;

  if (!obj1 || !obj2)
    return (obj1 == obj2);

  if (GET_OBJ_RNUM(obj1) != GET_OBJ_RNUM(obj2))
    return (FALSE);

  if (GET_OBJ_COST(obj1) != GET_OBJ_COST(obj2))
    return (FALSE);

  if (GET_OBJ_EXTRA(obj1) != GET_OBJ_EXTRA(obj2))
    return (FALSE);

  for (aindex = 0; aindex < MAX_OBJ_AFFECT; aindex++)
    if ((obj1->affected[aindex].location != obj2->affected[aindex].location) ||
	(obj1->affected[aindex].modifier != obj2->affected[aindex].modifier))
      return (FALSE);

  return (TRUE);
}


int shop_producing(struct obj_data *item, int shop_nr)
{
  int counter;

  if (GET_OBJ_RNUM(item) == NOTHING)
    return (FALSE);

  for (counter = 0; SHOP_PRODUCT(shop_nr, counter) != NOTHING; counter++)
    if (same_obj(item, &obj_proto[SHOP_PRODUCT(shop_nr, counter)]))
      return (TRUE);
  return (FALSE);
}


int transaction_amt(char *arg)
{
  char buf[MAX_INPUT_LENGTH];

  char *buywhat;

  /*
   * If we have two arguments, it means 'buy 5 3', or buy 5 of #3.
   * We don't do that if we only have one argument, like 'buy 5', buy #5.
   * Code from Andrey Fidrya <andrey@ALEX-UA.COM>
   */
  buywhat = one_argument(arg, buf);
  if (*buywhat && *buf && is_number(buf)) {
    strcpy(arg, arg + strlen(buf) + 1);	/* strcpy: OK (always smaller) */
    return (atoi(buf));
  }
  return (1);
}


char *times_message(struct obj_data *obj, char *name, int num)
{
  static char buf[256];
  size_t len;
  char *ptr;

  if (obj)
    len = strlcpy(buf, obj->short_description, sizeof(buf));
  else {
    if ((ptr = strchr(name, '.')) == NULL)
      ptr = name;
    else
      ptr++;
    len = snprintf(buf, sizeof(buf), "%s %s", AN(ptr), ptr);
  }

  if (num > 1 && len < sizeof(buf))
    snprintf(buf + len, sizeof(buf) - len, " (x %d)", num);

  return (buf);
}


struct obj_data *get_slide_obj_vis(struct char_data *ch, char *name,
				            struct obj_data *list)
{
  struct obj_data *i, *last_match = NULL;
  int j, number;
  char tmpname[MAX_INPUT_LENGTH];
  char *tmp;

  strlcpy(tmpname, name, sizeof(tmpname));
  tmp = tmpname;
  if (!(number = get_number(&tmp)))
    return (NULL);

  for (i = list, j = 1; i && (j <= number); i = i->next_content)
    if (isname(tmp, i->name))
      if (CAN_SEE_OBJ(ch, i) && !same_obj(last_match, i)) {
	if (j == number)
	  return (i);
	last_match = i;
	j++;
      }
  return (NULL);
}


struct obj_data *get_hash_obj_vis(struct char_data *ch, char *name,
				           struct obj_data *list)
{
  struct obj_data *loop, *last_obj = NULL;
  int qindex;

  if (is_number(name))
    qindex = atoi(name);
  else if (is_number(name + 1))
    qindex = atoi(name + 1);
  else
    return (NULL);

  for (loop = list; loop; loop = loop->next_content)
    if (CAN_SEE_OBJ(ch, loop) && GET_OBJ_COST(loop) > 0)
      if (!same_obj(last_obj, loop)) {
	if (--qindex == 0)
	  return (loop);
	last_obj = loop;
      }
  return (NULL);
}


struct obj_data *get_purchase_obj(struct char_data *ch, char *arg,
		            struct char_data *keeper, int shop_nr, int msg)
{
  char name[MAX_INPUT_LENGTH];
  struct obj_data *obj;

  one_argument(arg, name);
  do {
    if (*name == '#' || is_number(name))
      obj = get_hash_obj_vis(ch, name, keeper->carrying);
    else
      obj = get_slide_obj_vis(ch, name, keeper->carrying);
    if (!obj) {
      if (msg) {
        char buf[MAX_INPUT_LENGTH];

	snprintf(buf, sizeof(buf), shop_index[shop_nr].no_such_item1, GET_NAME(ch));
	do_tell(keeper, buf, cmd_tell, 0);
      }
      return (NULL);
    }
    if (GET_OBJ_COST(obj) <= 0) {
      extract_obj(obj);
      obj = NULL;
    }
  } while (!obj);
  return (obj);
}


/*
 * Shop purchase adjustment, based on charisma-difference from buyer to keeper.
 *
 * for i in `seq 15 -15`; do printf " * %3d: %6.4f\n" $i \
 * `echo "scale=4; 1+$i/70" | bc`; done
 *
 *  Shopkeeper higher charisma (markup)
 *  ^  15: 1.2142  14: 1.2000  13: 1.1857  12: 1.1714  11: 1.1571
 *  |  10: 1.1428   9: 1.1285   8: 1.1142   7: 1.1000   6: 1.0857
 *  |   5: 1.0714   4: 1.0571   3: 1.0428   2: 1.0285   1: 1.0142
 *  +   0: 1.0000
 *  |  -1: 0.9858  -2: 0.9715  -3: 0.9572  -4: 0.9429  -5: 0.9286
 *  |  -6: 0.9143  -7: 0.9000  -8: 0.8858  -9: 0.8715 -10: 0.8572
 *  v -11: 0.8429 -12: 0.8286 -13: 0.8143 -14: 0.8000 -15: 0.7858
 *  Player higher charisma (discount)
 *
 * Most mobiles have 11 charisma so an 18 charisma player would get a 10%
 * discount beyond the basic price.  That assumes they put a lot of points
 * into charisma, because on the flip side they'd get 11% inflation by
 * having a 3.
 */
int buy_price(struct obj_data *obj, int shop_nr, struct char_data *keeper, struct char_data *buyer)
{
  return (int) (GET_OBJ_COST(obj) * SHOP_BUYPROFIT(shop_nr)
	* (1 + (GET_CHA(keeper) - GET_CHA(buyer)) / (float)70));
}

/*
 * When the shopkeeper is buying, we reverse the discount. Also make sure
 * we don't buy for more than we sell for, to prevent infinite money-making.
 */
int sell_price(struct obj_data *obj, int shop_nr, struct char_data *keeper, struct char_data *seller)
{
  float sell_cost_modifier = SHOP_SELLPROFIT(shop_nr) * (1 - (GET_CHA(keeper) - GET_CHA(seller)) / (float)70);
  float buy_cost_modifier = SHOP_BUYPROFIT(shop_nr) * (1 + (GET_CHA(keeper) - GET_CHA(seller)) / (float)70);

  if (sell_cost_modifier > buy_cost_modifier)
    sell_cost_modifier = buy_cost_modifier;

  return (int) (GET_OBJ_COST(obj) * sell_cost_modifier);
}


void shopping_buy(char *arg, struct char_data *ch, struct char_data *keeper, int shop_nr)
{
  char tempstr[MAX_INPUT_LENGTH], tempbuf[MAX_INPUT_LENGTH];
  struct obj_data *obj, *last_obj = NULL;
  int goldamt = 0, buynum, bought = 0;

  if (!is_ok(keeper, ch, shop_nr))
    return;

  if (SHOP_SORT(shop_nr) < IS_CARRYING_N(keeper))
    sort_keeper_objs(keeper, shop_nr);

  if ((buynum = transaction_amt(arg)) < 0) {
    char buf[MAX_INPUT_LENGTH];

    snprintf(buf, sizeof(buf), "%s A negative amount?  Try selling me something.",
	    GET_NAME(ch));
    do_tell(keeper, buf, cmd_tell, 0);
    return;
  }
  if (!*arg || !buynum) {
    char buf[MAX_INPUT_LENGTH];

    snprintf(buf, sizeof(buf), "%s What do you want to buy??", GET_NAME(ch));
    do_tell(keeper, buf, cmd_tell, 0);
    return;
  }
  if (!(obj = get_purchase_obj(ch, arg, keeper, shop_nr, TRUE)))
    return;

  if (buy_price(obj, shop_nr, keeper, ch) > GET_GOLD(ch) && !IS_GOD(ch)) {
    char actbuf[MAX_INPUT_LENGTH];

    snprintf(actbuf, sizeof(actbuf), shop_index[shop_nr].missing_cash2, GET_NAME(ch));
    do_tell(keeper, actbuf, cmd_tell, 0);

    switch (SHOP_BROKE_TEMPER(shop_nr)) {
    case 0:
      do_action(keeper, strcpy(actbuf, GET_NAME(ch)), cmd_puke, 0);	/* strcpy: OK (MAX_NAME_LENGTH < MAX_INPUT_LENGTH) */
      return;
    case 1:
      do_echo(keeper, strcpy(actbuf, "smokes on his joint."), cmd_emote, SCMD_EMOTE);	/* strcpy: OK */
      return;
    default:
      return;
    }
  }
  if (IS_CARRYING_N(ch) + 1 > CAN_CARRY_N(ch)) {
    send_to_char(ch, "%s: You can't carry any more items.\r\n", fname(obj->name));
    return;
  }
  if (IS_CARRYING_W(ch) + GET_OBJ_WEIGHT(obj) > CAN_CARRY_W(ch)) {
    send_to_char(ch, "%s: You can't carry that much weight.\r\n", fname(obj->name));
    return;
  }
  while (obj && (GET_GOLD(ch) >= buy_price(obj, shop_nr, keeper, ch) || IS_GOD(ch))
	 && IS_CARRYING_N(ch) < CAN_CARRY_N(ch) && bought < buynum
	 && IS_CARRYING_W(ch) + GET_OBJ_WEIGHT(obj) <= CAN_CARRY_W(ch)) {
    int charged;

    bought++;
    /* Test if producing shop ! */
    if (shop_producing(obj, shop_nr))
      obj = read_object(GET_OBJ_RNUM(obj), REAL);
    else {
      obj_from_char(obj);
      SHOP_SORT(shop_nr)--;
    }
    obj_to_char(obj, ch);

    charged = buy_price(obj, shop_nr, keeper, ch);
    goldamt += charged;
    if (!IS_GOD(ch))
      GET_GOLD(ch) -= charged;

    last_obj = obj;
    obj = get_purchase_obj(ch, arg, keeper, shop_nr, FALSE);
    if (!same_obj(obj, last_obj))
      break;
  }

  if (bought < buynum) {
    char buf[MAX_INPUT_LENGTH];

    if (!obj || !same_obj(last_obj, obj))
      snprintf(buf, sizeof(buf), "%s I only have %d to sell you.", GET_NAME(ch), bought);
    else if (GET_GOLD(ch) < buy_price(obj, shop_nr, keeper, ch))
      snprintf(buf, sizeof(buf), "%s You can only afford %d.", GET_NAME(ch), bought);
    else if (IS_CARRYING_N(ch) >= CAN_CARRY_N(ch))
      snprintf(buf, sizeof(buf), "%s You can only hold %d.", GET_NAME(ch), bought);
    else if (IS_CARRYING_W(ch) + GET_OBJ_WEIGHT(obj) > CAN_CARRY_W(ch))
      snprintf(buf, sizeof(buf), "%s You can only carry %d.", GET_NAME(ch), bought);
    else
      snprintf(buf, sizeof(buf), "%s Something screwy only gave you %d.", GET_NAME(ch), bought);
    do_tell(keeper, buf, cmd_tell, 0);
  }
  if (!IS_GOD(ch))
    GET_GOLD(keeper) += goldamt;

  strlcpy(tempstr, times_message(ch->carrying, 0, bought), sizeof(tempstr));

  snprintf(tempbuf, sizeof(tempbuf), "$n buys %s.", tempstr);
  act(tempbuf, FALSE, ch, obj, 0, TO_ROOM);

  snprintf(tempbuf, sizeof(tempbuf), shop_index[shop_nr].message_buy, GET_NAME(ch), goldamt);
  do_tell(keeper, tempbuf, cmd_tell, 0);

  send_to_char(ch, "You now have %s.\r\n", tempstr);

  if (SHOP_USES_BANK(shop_nr))
    if (GET_GOLD(keeper) > MAX_OUTSIDE_BANK) {
      SHOP_BANK(shop_nr) += (GET_GOLD(keeper) - MAX_OUTSIDE_BANK);
      GET_GOLD(keeper) = MAX_OUTSIDE_BANK;
    }
}


struct obj_data *get_selling_obj(struct char_data *ch, char *name, struct char_data *keeper, int shop_nr, int msg)
{
  char buf[MAX_INPUT_LENGTH];
  struct obj_data *obj;
  int result;

  if (!(obj = get_obj_in_list_vis(ch, name, NULL, ch->carrying))) {
    if (msg) {
      char tbuf[MAX_INPUT_LENGTH];

      snprintf(tbuf, sizeof(tbuf), shop_index[shop_nr].no_such_item2, GET_NAME(ch));
      do_tell(keeper, tbuf, cmd_tell, 0);
    }
    return (NULL);
  }
  if ((result = trade_with(obj, shop_nr)) == OBJECT_OK)
    return (obj);

  if (!msg)
    return (0);

  switch (result) {
  case OBJECT_NOVAL:
    snprintf(buf, sizeof(buf), "%s You've got to be kidding, that thing is worthless!", GET_NAME(ch));
    break;
  case OBJECT_NOTOK:
    snprintf(buf, sizeof(buf), shop_index[shop_nr].do_not_buy, GET_NAME(ch));
    break;
  case OBJECT_DEAD:
    snprintf(buf, sizeof(buf), "%s %s", GET_NAME(ch), MSG_NO_USED_WANDSTAFF);
    break;
  default:
    log("SYSERR: Illegal return value of %d from trade_with() (%s)", result, __FILE__);	/* Someone might rename it... */
    snprintf(buf, sizeof(buf), "%s An error has occurred.", GET_NAME(ch));
    break;
  }
  do_tell(keeper, buf, cmd_tell, 0);
  return (NULL);
}


struct obj_data *slide_obj(struct obj_data *obj, struct char_data *keeper,
			            int shop_nr)
/*
   This function is a slight hack!  To make sure that duplicate items are
   only listed once on the "list", this function groups "identical"
   objects together on the shopkeeper's inventory list.  The hack involves
   knowing how the list is put together, and manipulating the order of
   the objects on the list.  (But since most of DIKU is not encapsulated,
   and information hiding is almost never used, it isn't that big a deal) -JF
*/
{
  struct obj_data *loop;
  int temp;

  if (SHOP_SORT(shop_nr) < IS_CARRYING_N(keeper))
    sort_keeper_objs(keeper, shop_nr);

  /* Extract the object if it is identical to one produced */
  if (shop_producing(obj, shop_nr)) {
    temp = GET_OBJ_RNUM(obj);
    extract_obj(obj);
    return (&obj_proto[temp]);
  }
  SHOP_SORT(shop_nr)++;
  loop = keeper->carrying;
  obj_to_char(obj, keeper);
  keeper->carrying = loop;
  while (loop) {
    if (same_obj(obj, loop)) {
      obj->next_content = loop->next_content;
      loop->next_content = obj;
      return (obj);
    }
    loop = loop->next_content;
  }
  keeper->carrying = obj;
  return (obj);
}


void sort_keeper_objs(struct char_data *keeper, int shop_nr)
{
  struct obj_data *list = NULL, *temp;

  while (SHOP_SORT(shop_nr) < IS_CARRYING_N(keeper)) {
    temp = keeper->carrying;
    obj_from_char(temp);
    temp->next_content = list;
    list = temp;
  }

  while (list) {
    temp = list;
    list = list->next_content;
    if (shop_producing(temp, shop_nr) &&
	!get_obj_in_list_num(GET_OBJ_RNUM(temp), keeper->carrying)) {
      obj_to_char(temp, keeper);
      SHOP_SORT(shop_nr)++;
    } else
      slide_obj(temp, keeper, shop_nr);
  }
}


void shopping_sell(char *arg, struct char_data *ch, struct char_data *keeper, int shop_nr)
{
  char tempstr[MAX_INPUT_LENGTH], name[MAX_INPUT_LENGTH], tempbuf[MAX_INPUT_LENGTH];
  struct obj_data *obj;
  int sellnum, sold = 0, goldamt = 0;

  if (!(is_ok(keeper, ch, shop_nr)))
    return;

  if ((sellnum = transaction_amt(arg)) < 0) {
    char buf[MAX_INPUT_LENGTH];

    snprintf(buf, sizeof(buf), "%s A negative amount?  Try buying something.", GET_NAME(ch));
    do_tell(keeper, buf, cmd_tell, 0);
    return;
  }
  if (!*arg || !sellnum) {
    char buf[MAX_INPUT_LENGTH];

    snprintf(buf, sizeof(buf), "%s What do you want to sell??", GET_NAME(ch));
    do_tell(keeper, buf, cmd_tell, 0);
    return;
  }
  one_argument(arg, name);
  if (!(obj = get_selling_obj(ch, name, keeper, shop_nr, TRUE)))
    return;

  if (GET_GOLD(keeper) + SHOP_BANK(shop_nr) < sell_price(obj, shop_nr, keeper, ch)) {
    char buf[MAX_INPUT_LENGTH];

    snprintf(buf, sizeof(buf), shop_index[shop_nr].missing_cash1, GET_NAME(ch));
    do_tell(keeper, buf, cmd_tell, 0);
    return;
  }
  while (obj && GET_GOLD(keeper) + SHOP_BANK(shop_nr) >= sell_price(obj, shop_nr, keeper, ch) && sold < sellnum) {
    int charged = sell_price(obj, shop_nr, keeper, ch);

    goldamt += charged;
    GET_GOLD(keeper) -= charged;

    sold++;
    obj_from_char(obj);
    slide_obj(obj, keeper, shop_nr);	/* Seems we don't use return value. */
    obj = get_selling_obj(ch, name, keeper, shop_nr, FALSE);
  }

  if (sold < sellnum) {
    char buf[MAX_INPUT_LENGTH];

    if (!obj)
      snprintf(buf, sizeof(buf), "%s You only have %d of those.", GET_NAME(ch), sold);
    else if (GET_GOLD(keeper) + SHOP_BANK(shop_nr) < sell_price(obj, shop_nr, keeper, ch))
      snprintf(buf, sizeof(buf), "%s I can only afford to buy %d of those.", GET_NAME(ch), sold);
    else
      snprintf(buf, sizeof(buf), "%s Something really screwy made me buy %d.", GET_NAME(ch), sold);

    do_tell(keeper, buf, cmd_tell, 0);
  }
  GET_GOLD(ch) += goldamt;

  strlcpy(tempstr, times_message(0, name, sold), sizeof(tempstr));
  snprintf(tempbuf, sizeof(tempbuf), "$n sells %s.", tempstr);
  act(tempbuf, FALSE, ch, obj, 0, TO_ROOM);

  snprintf(tempbuf, sizeof(tempbuf), shop_index[shop_nr].message_sell, GET_NAME(ch), goldamt);
  do_tell(keeper, tempbuf, cmd_tell, 0);

  send_to_char(ch, "The shopkeeper now has %s.\r\n", tempstr);

  if (GET_GOLD(keeper) < MIN_OUTSIDE_BANK) {
    goldamt = MIN(MAX_OUTSIDE_BANK - GET_GOLD(keeper), SHOP_BANK(shop_nr));
    SHOP_BANK(shop_nr) -= goldamt;
    GET_GOLD(keeper) += goldamt;
  }
}


void shopping_value(char *arg, struct char_data *ch, struct char_data *keeper, int shop_nr)
{
  char buf[MAX_STRING_LENGTH], name[MAX_INPUT_LENGTH];
  struct obj_data *obj;

  if (!is_ok(keeper, ch, shop_nr))
    return;

  if (!*arg) {
    snprintf(buf, sizeof(buf), "%s What do you want me to evaluate??", GET_NAME(ch));
    do_tell(keeper, buf, cmd_tell, 0);
    return;
  }
  one_argument(arg, name);
  if (!(obj = get_selling_obj(ch, name, keeper, shop_nr, TRUE)))
    return;

  snprintf(buf, sizeof(buf), "%s I'll give you %d gold coins for that!", GET_NAME(ch), sell_price(obj, shop_nr, keeper, ch));
  do_tell(keeper, buf, cmd_tell, 0);
}


char *list_object(struct obj_data *obj, int cnt, int aindex, int shop_nr, struct char_data *keeper, struct char_data *ch)
{
  static char result[256];
  char	itemname[128],
	quantity[16];	/* "Unlimited" or "%d" */

  if (shop_producing(obj, shop_nr))
    strcpy(quantity, "Unlimited");	/* strcpy: OK (for 'quantity >= 10') */
  else
    sprintf(quantity, "%d", cnt);	/* sprintf: OK (for 'quantity >= 11', 32-bit int) */

  switch (GET_OBJ_TYPE(obj)) {
  case ITEM_DRINKCON:
    if (GET_OBJ_VAL(obj, 1))
      snprintf(itemname, sizeof(itemname), "%s of %s", obj->short_description, drinks[GET_OBJ_VAL(obj, 2)]);
    else
      strlcpy(itemname, obj->short_description, sizeof(itemname));
    break;

  case ITEM_WAND:
  case ITEM_STAFF:
    snprintf(itemname, sizeof(itemname), "%s%s", obj->short_description,
	GET_OBJ_VAL(obj, 2) < GET_OBJ_VAL(obj, 1) ? " (partially used)" : "");
    break;

  default:
    strlcpy(itemname, obj->short_description, sizeof(itemname));
    break;
  }
  CAP(itemname);

  snprintf(result, sizeof(result), " %2d)  %9s   %-48s %6d\r\n",
	aindex, quantity, itemname, buy_price(obj, shop_nr, keeper, ch));
  return (result);
}


void shopping_list(char *arg, struct char_data *ch, struct char_data *keeper, int shop_nr)
{
  char buf[MAX_STRING_LENGTH], name[MAX_INPUT_LENGTH];
  struct obj_data *obj, *last_obj = NULL;
  int cnt = 0, lindex = 0, found = FALSE;
  size_t len;
  /* cnt is the number of that particular object available */

  if (!is_ok(keeper, ch, shop_nr))
    return;

  if (SHOP_SORT(shop_nr) < IS_CARRYING_N(keeper))
    sort_keeper_objs(keeper, shop_nr);

  one_argument(arg, name);

  len = strlcpy(buf,   " ##   Available   Item                                               Cost\r\n"
		"-------------------------------------------------------------------------\r\n", sizeof(buf));
  if (keeper->carrying)
    for (obj = keeper->carrying; obj; obj = obj->next_content)
      if (CAN_SEE_OBJ(ch, obj) && GET_OBJ_COST(obj) > 0) {
	if (!last_obj) {
	  last_obj = obj;
	  cnt = 1;
	} else if (same_obj(last_obj, obj))
	  cnt++;
	else {
	  lindex++;
	  if (!*name || isname(name, last_obj->name)) {
	    strncat(buf, list_object(last_obj, cnt, lindex, shop_nr, keeper, ch), sizeof(buf) - len - 1);	/* strncat: OK */
            len = strlen(buf);
            if (len + 1 >= sizeof(buf))
              break;
            found = TRUE;
          }
	  cnt = 1;
	  last_obj = obj;
	}
      }
  lindex++;
  if (!last_obj)	/* we actually have nothing in our list for sale, period */
    send_to_char(ch, "Currently, there is nothing for sale.\r\n");
  else if (*name && !found)	/* nothing the char was looking for was found */
    send_to_char(ch, "Presently, none of those are for sale.\r\n");
  else {
    if (!*name || isname(name, last_obj->name))	/* show last obj */
      if (len < sizeof(buf))
        strncat(buf, list_object(last_obj, cnt, lindex, shop_nr, keeper, ch), sizeof(buf) - len - 1);	/* strncat: OK */
    page_string(ch->desc, buf, TRUE);
  }
}


int ok_shop_room(int shop_nr, room_vnum room)
{
  int mindex;

  for (mindex = 0; SHOP_ROOM(shop_nr, mindex) != NOWHERE; mindex++)
    if (SHOP_ROOM(shop_nr, mindex) == room)
      return (TRUE);
  return (FALSE);
}


SPECIAL(shop_keeper)
{
  struct char_data *keeper = (struct char_data *)me;
  int shop_nr;

  for (shop_nr = 0; shop_nr <= top_shop; shop_nr++)
    if (SHOP_KEEPER(shop_nr) == keeper->nr)
      break;

  if (shop_nr > top_shop)
    return (FALSE);

  if (SHOP_FUNC(shop_nr))	/* Check secondary function */
    if ((SHOP_FUNC(shop_nr)) (ch, me, cmd, argument))
      return (TRUE);

  if (keeper == ch) {
    if (cmd)
      SHOP_SORT(shop_nr) = 0;	/* Safety in case "drop all" */
    return (FALSE);
  }
  if (!ok_shop_room(shop_nr, GET_ROOM_VNUM(IN_ROOM(ch))))
    return (0);

  if (!AWAKE(keeper))
    return (FALSE);

  if (CMD_IS("steal")) {
    char argm[MAX_INPUT_LENGTH];

    snprintf(argm, sizeof(argm), "$N shouts '%s'", MSG_NO_STEAL_HERE);
    act(argm, FALSE, ch, 0, keeper, TO_CHAR);

    do_action(keeper, GET_NAME(ch), cmd_slap, 0);
    return (TRUE);
  }

  if (CMD_IS("buy")) {
    shopping_buy(argument, ch, keeper, shop_nr);
    return (TRUE);
  } else if (CMD_IS("sell")) {
    shopping_sell(argument, ch, keeper, shop_nr);
    return (TRUE);
  } else if (CMD_IS("value")) {
    shopping_value(argument, ch, keeper, shop_nr);
    return (TRUE);
  } else if (CMD_IS("list")) {
    shopping_list(argument, ch, keeper, shop_nr);
    return (TRUE);
  }
  return (FALSE);
}


int ok_damage_shopkeeper(struct char_data *ch, struct char_data *victim)
{
  int sindex;

  if (!IS_MOB(victim) || mob_index[GET_MOB_RNUM(victim)].func != shop_keeper)
    return (TRUE);

  /* Prevent "invincible" shopkeepers if they're charmed. */
  if (AFF_FLAGGED(victim, AFF_CHARM))
    return (TRUE);

  for (sindex = 0; sindex <= top_shop; sindex++)
    if (GET_MOB_RNUM(victim) == SHOP_KEEPER(sindex) && !SHOP_KILL_CHARS(sindex)) {
      char buf[MAX_INPUT_LENGTH];

      snprintf(buf, sizeof(buf), "%s %s", GET_NAME(ch), MSG_CANT_KILL_KEEPER);
      do_tell(victim, buf, cmd_tell, 0);

      do_action(victim, GET_NAME(ch), cmd_slap, 0);
      return (FALSE);
    }

  return (TRUE);
}


/* val == obj_vnum and obj_rnum (?) */
int add_to_list(struct shop_buy_data *list, int type, int *len, int *val)
{
  if (*val != NOTHING) {
    if (*len < MAX_SHOP_OBJ) {
      if (type == LIST_PRODUCE)
	*val = real_object(*val);
      if (*val != NOTHING) {
	BUY_TYPE(list[*len]) = *val;
	BUY_WORD(list[(*len)++]) = NULL;
      } else
	*val = NOTHING;
      return (FALSE);
    } else
      return (TRUE);
  }
  return (FALSE);
}


int end_read_list(struct shop_buy_data *list, int len, int error)
{
  if (error)
    log("SYSERR: Raise MAX_SHOP_OBJ constant in shop.h to %d", len + error);
  BUY_WORD(list[len]) = NULL;
  BUY_TYPE(list[len++]) = NOTHING;
  return (len);
}


void read_line(FILE *shop_f, const char *string, void *data)
{
  char buf[READ_SIZE];

  if (!get_line(shop_f, buf) || !sscanf(buf, string, data)) {
    log("SYSERR: Error in shop #%d, near '%s' with '%s'", SHOP_NUM(top_shop), buf, string);
    exit(1);
  }
}


int read_list(FILE *shop_f, struct shop_buy_data *list, int new_format,
	          int max, int type)
{
  int count, temp, len = 0, error = 0;

  if (new_format) {
    for (;;) {
      read_line(shop_f, "%d", &temp);
      if (temp < 0)	/* Always "-1" the string. */
        break;
      error += add_to_list(list, type, &len, &temp);
    }
  } else
    for (count = 0; count < max; count++) {
      read_line(shop_f, "%d", &temp);
      error += add_to_list(list, type, &len, &temp);
    }
  return (end_read_list(list, len, error));
}


/* END_OF inefficient. */
int read_type_list(FILE *shop_f, struct shop_buy_data *list,
		       int new_format, int max)
{
  int tindex, num, len = 0, error = 0;
  char *ptr;
  char buf[MAX_STRING_LENGTH];

  if (!new_format)
    return (read_list(shop_f, list, 0, max, LIST_TRADE));

  do {
    fgets(buf, sizeof(buf), shop_f);
    if ((ptr = strchr(buf, ';')) != NULL)
      *ptr = '\0';
    else
      *(END_OF(buf) - 1) = '\0';

    num = NOTHING;

    if (strncmp(buf, "-1", 4) != 0)
      for (tindex = 0; *item_types[tindex] != '\n'; tindex++)
        if (!strn_cmp(item_types[tindex], buf, strlen(item_types[tindex]))) {
          num = tindex;
          strcpy(buf, buf + strlen(item_types[tindex]));	/* strcpy: OK (always smaller) */
          break;
        }

    ptr = buf;
    if (num == NOTHING) {
      sscanf(buf, "%d", &num);
      while (!isdigit(*ptr))
	ptr++;
      while (isdigit(*ptr))
	ptr++;
    }
    while (isspace(*ptr))
      ptr++;
    while (isspace(*(END_OF(ptr) - 1)))
      *(END_OF(ptr) - 1) = '\0';
    error += add_to_list(list, LIST_TRADE, &len, &num);
    if (*ptr)
      BUY_WORD(list[len - 1]) = strdup(ptr);
  } while (num >= 0);
  return (end_read_list(list, len, error));
}


char *read_shop_message(int mnum, room_vnum shr, FILE *shop_f, const char *why)
{
  int cht, ss = 0, ds = 0, err = 0;
  char *tbuf;

  if (!(tbuf = fread_string(shop_f, why)))
    return (NULL);

  for (cht = 0; tbuf[cht]; cht++) {
    if (tbuf[cht] != '%')
      continue;

    if (tbuf[cht + 1] == 's')
      ss++;
    else if (tbuf[cht + 1] == 'd' && (mnum == 5 || mnum == 6)) {
      if (ss == 0) {
        log("SYSERR: Shop #%d has %%d before %%s, message #%d.", shr, mnum);
        err++;
      }
      ds++;
    } else if (tbuf[cht + 1] != '%') {
      log("SYSERR: Shop #%d has invalid format '%%%c' in message #%d.", shr, tbuf[cht + 1], mnum);
      err++;
    }
  }

  if (ss > 1 || ds > 1) {
    log("SYSERR: Shop #%d has too many specifiers for message #%d. %%s=%d %%d=%d", shr, mnum, ss, ds);
    err++;
  }

  if (err) {
    free(tbuf);
    return (NULL);
  }
  return (tbuf);
}


void boot_the_shops(FILE *shop_f, char *filename, int rec_count)
{
  char *buf, buf2[256];
  int temp, count, new_format = FALSE;
  struct shop_buy_data list[MAX_SHOP_OBJ + 1];
  int done = FALSE;

  snprintf(buf2, sizeof(buf2), "beginning of shop file %s", filename);

  while (!done) {
    buf = fread_string(shop_f, buf2);
    if (*buf == '#') {		/* New shop */
      sscanf(buf, "#%d\n", &temp);
      snprintf(buf2, sizeof(buf2), "shop #%d in shop file %s", temp, filename);
      free(buf);		/* Plug memory leak! */
      top_shop++;
      if (!top_shop)
	CREATE(shop_index, struct shop_data, rec_count);
      SHOP_NUM(top_shop) = temp;
      temp = read_list(shop_f, list, new_format, MAX_PROD, LIST_PRODUCE);
      CREATE(shop_index[top_shop].producing, obj_vnum, temp);
      for (count = 0; count < temp; count++)
	SHOP_PRODUCT(top_shop, count) = BUY_TYPE(list[count]);

      read_line(shop_f, "%f", &SHOP_BUYPROFIT(top_shop));
      read_line(shop_f, "%f", &SHOP_SELLPROFIT(top_shop));

      temp = read_type_list(shop_f, list, new_format, MAX_TRADE);
      CREATE(shop_index[top_shop].type, struct shop_buy_data, temp);
      for (count = 0; count < temp; count++) {
	SHOP_BUYTYPE(top_shop, count) = BUY_TYPE(list[count]);
	SHOP_BUYWORD(top_shop, count) = BUY_WORD(list[count]);
      }

      shop_index[top_shop].no_such_item1 = read_shop_message(0, SHOP_NUM(top_shop), shop_f, buf2);
      shop_index[top_shop].no_such_item2 = read_shop_message(1, SHOP_NUM(top_shop), shop_f, buf2);
      shop_index[top_shop].do_not_buy = read_shop_message(2, SHOP_NUM(top_shop), shop_f, buf2);
      shop_index[top_shop].missing_cash1 = read_shop_message(3, SHOP_NUM(top_shop), shop_f, buf2);
      shop_index[top_shop].missing_cash2 = read_shop_message(4, SHOP_NUM(top_shop), shop_f, buf2);
      shop_index[top_shop].message_buy = read_shop_message(5, SHOP_NUM(top_shop), shop_f, buf2);
      shop_index[top_shop].message_sell = read_shop_message(6, SHOP_NUM(top_shop), shop_f, buf2);
      read_line(shop_f, "%d", &SHOP_BROKE_TEMPER(top_shop));
      read_line(shop_f, "%d", &SHOP_BITVECTOR(top_shop));
      read_line(shop_f, "%hd", &SHOP_KEEPER(top_shop));

      SHOP_KEEPER(top_shop) = real_mobile(SHOP_KEEPER(top_shop));
      read_line(shop_f, "%d", &SHOP_TRADE_WITH(top_shop));

      temp = read_list(shop_f, list, new_format, 1, LIST_ROOM);
      CREATE(shop_index[top_shop].in_room, room_vnum, temp);
      for (count = 0; count < temp; count++)
	SHOP_ROOM(top_shop, count) = BUY_TYPE(list[count]);

      read_line(shop_f, "%d", &SHOP_OPEN1(top_shop));
      read_line(shop_f, "%d", &SHOP_CLOSE1(top_shop));
      read_line(shop_f, "%d", &SHOP_OPEN2(top_shop));
      read_line(shop_f, "%d", &SHOP_CLOSE2(top_shop));

      SHOP_BANK(top_shop) = 0;
      SHOP_SORT(top_shop) = 0;
      SHOP_FUNC(top_shop) = NULL;
    } else {
      if (*buf == '$')		/* EOF */
	done = TRUE;
      else if (strstr(buf, VERSION3_TAG))	/* New format marker */
	new_format = TRUE;
      free(buf);		/* Plug memory leak! */
    }
  }
}


void assign_the_shopkeepers(void)
{
  int cindex;

  cmd_say = find_command("say");
  cmd_tell = find_command("tell");
  cmd_emote = find_command("emote");
  cmd_slap = find_command("slap");
  cmd_puke = find_command("puke");

  for (cindex = 0; cindex <= top_shop; cindex++) {
    if (SHOP_KEEPER(cindex) == NOBODY)
      continue;

    /* Having SHOP_FUNC() as 'shop_keeper' will cause infinite recursion. */
    if (mob_index[SHOP_KEEPER(cindex)].func && mob_index[SHOP_KEEPER(cindex)].func != shop_keeper)
      SHOP_FUNC(cindex) = mob_index[SHOP_KEEPER(cindex)].func;

    mob_index[SHOP_KEEPER(cindex)].func = shop_keeper;
  }
}


char *customer_string(int shop_nr, int detailed)
{
  int sindex = 0, flag = 1, nlen;
  size_t len = 0;
  static char buf[256];

  while (*trade_letters[sindex] != '\n' && len + 1 < sizeof(buf)) {
    if (detailed) {
      if (!IS_SET(flag, SHOP_TRADE_WITH(shop_nr))) {
	nlen = snprintf(buf + len, sizeof(buf) - len, ", %s", trade_letters[sindex]);

        if (len + nlen >= sizeof(buf) || nlen < 0)
          break;

        len += nlen;
      }
    } else {
      buf[len++] = (IS_SET(flag, SHOP_TRADE_WITH(shop_nr)) ? '_' : *trade_letters[sindex]);
      buf[len] = '\0';

      if (len >= sizeof(buf))
        break;
    }

    sindex++;
    flag <<= 1;
  }

  buf[sizeof(buf) - 1] = '\0';
  return (buf);
}


/* END_OF inefficient */
void list_all_shops(struct char_data *ch)
{
  const char *list_all_shops_header =
	" ##   Virtual   Where    Keeper    Buy   Sell   Customers\r\n"
	"---------------------------------------------------------\r\n";
  int shop_nr, headerlen = strlen(list_all_shops_header);
  size_t len = 0;
  char buf[MAX_STRING_LENGTH], buf1[16];

  *buf = '\0';
  for (shop_nr = 0; shop_nr <= top_shop && len < sizeof(buf); shop_nr++) {
    /* New page in page_string() mechanism, print the header again. */
    if (!(shop_nr % (PAGE_LENGTH - 2))) {
      /*
       * If we don't have enough room for the header, or all we have room left
       * for is the header, then don't add it and just quit now.
       */
      if (len + headerlen + 1 >= sizeof(buf))
        break;
      strcpy(buf + len, list_all_shops_header);	/* strcpy: OK (length checked above) */
      len += headerlen;
    }

    if (SHOP_KEEPER(shop_nr) == NOBODY)
      strcpy(buf1, "<NONE>");	/* strcpy: OK (for 'buf1 >= 7') */
    else
      sprintf(buf1, "%6d", mob_index[SHOP_KEEPER(shop_nr)].vnum);	/* sprintf: OK (for 'buf1 >= 11', 32-bit int) */

    len += snprintf(buf + len, sizeof(buf) - len,
               "%3d   %6d   %6d    %s   %3.2f   %3.2f    %s\r\n",
	       shop_nr + 1, SHOP_NUM(shop_nr), SHOP_ROOM(shop_nr, 0), buf1,
	       SHOP_SELLPROFIT(shop_nr), SHOP_BUYPROFIT(shop_nr),
	       customer_string(shop_nr, FALSE));
  }

  page_string(ch->desc, buf, TRUE);
}


void list_detailed_shop(struct char_data *ch, int shop_nr)
{
  struct char_data *k;
  int sindex, column;
  char *ptrsave;

  send_to_char(ch, "Vnum:       [%5d], Rnum: [%5d]\r\n", SHOP_NUM(shop_nr), shop_nr + 1);


  send_to_char(ch, "Rooms:      ");
  column = 12;	/* ^^^ strlen ^^^ */
  for (sindex = 0; SHOP_ROOM(shop_nr, sindex) != NOWHERE; sindex++) {
    char buf1[128];
    int linelen, temp;

    if (sindex) {
      send_to_char(ch, ", ");
      column += 2;
    }

    if ((temp = real_room(SHOP_ROOM(shop_nr, sindex))) != NOWHERE)
      linelen = snprintf(buf1, sizeof(buf1), "%s (#%d)", world[temp].name, GET_ROOM_VNUM(temp));
    else
      linelen = snprintf(buf1, sizeof(buf1), "<UNKNOWN> (#%d)", SHOP_ROOM(shop_nr, sindex));

    /* Implementing word-wrapping: assumes screen-size == 80 */
    if (linelen + column >= 78 && column >= 20) {
      send_to_char(ch, "\r\n            ");
      /* 12 is to line up with "Rooms:" printed first, and spaces above. */
      column = 12;
    }

    if (!send_to_char(ch, "%s", buf1))
      return;
    column += linelen;
  }
  if (!sindex)
    send_to_char(ch, "Rooms:      None!");

  send_to_char(ch, "\r\nShopkeeper: ");
  if (SHOP_KEEPER(shop_nr) != NOBODY) {
    send_to_char(ch, "%s (#%d), Special Function: %s\r\n",
	GET_NAME(&mob_proto[SHOP_KEEPER(shop_nr)]),
	mob_index[SHOP_KEEPER(shop_nr)].vnum,
	YESNO(SHOP_FUNC(shop_nr)));

    if ((k = get_char_num(SHOP_KEEPER(shop_nr))))
      send_to_char(ch, "Coins:      [%9d], Bank: [%9d] (Total: %d)\r\n",
	 GET_GOLD(k), SHOP_BANK(shop_nr), GET_GOLD(k) + SHOP_BANK(shop_nr));
  } else
    send_to_char(ch, "<NONE>\r\n");


  send_to_char(ch, "Customers:  %s\r\n", (ptrsave = customer_string(shop_nr, TRUE)) ? ptrsave : "None");


  send_to_char(ch, "Produces:   ");
  column = 12;	/* ^^^ strlen ^^^ */
  for (sindex = 0; SHOP_PRODUCT(shop_nr, sindex) != NOTHING; sindex++) {
    char buf1[128];
    int linelen;

    if (sindex) {
      send_to_char(ch, ", ");
      column += 2;
    }
    linelen = snprintf(buf1, sizeof(buf1), "%s (#%d)",
		obj_proto[SHOP_PRODUCT(shop_nr, sindex)].short_description,
		obj_index[SHOP_PRODUCT(shop_nr, sindex)].vnum);

    /* Implementing word-wrapping: assumes screen-size == 80 */
    if (linelen + column >= 78 && column >= 20) {
      send_to_char(ch, "\r\n            ");
      /* 12 is to line up with "Produces:" printed first, and spaces above. */
      column = 12;
    }

    if (!send_to_char(ch, "%s", buf1))
      return;
    column += linelen;
  }
  if (!sindex)
    send_to_char(ch, "Produces:   Nothing!");

  send_to_char(ch, "\r\nBuys:       ");
  column = 12;	/* ^^^ strlen ^^^ */
  for (sindex = 0; SHOP_BUYTYPE(shop_nr, sindex) != NOTHING; sindex++) {
    char buf1[128];
    size_t linelen;

    if (sindex) {
      send_to_char(ch, ", ");
      column += 2;
    }

    linelen = snprintf(buf1, sizeof(buf1), "%s (#%d) [%s]",
		item_types[SHOP_BUYTYPE(shop_nr, sindex)],
		SHOP_BUYTYPE(shop_nr, sindex),
		SHOP_BUYWORD(shop_nr, sindex) ? SHOP_BUYWORD(shop_nr, sindex) : "all");

    /* Implementing word-wrapping: assumes screen-size == 80 */
    if (linelen + column >= 78 && column >= 20) {
      send_to_char(ch, "\r\n            ");
      /* 12 is to line up with "Buys:" printed first, and spaces above. */
      column = 12;
    }

    if (!send_to_char(ch, "%s", buf1))
      return;
    column += linelen;
  }
  if (!sindex)
    send_to_char(ch, "Buys:       Nothing!");

  send_to_char(ch, "\r\nBuy at:     [%4.2f], Sell at: [%4.2f], Open: [%d-%d, %d-%d]\r\n",
	SHOP_SELLPROFIT(shop_nr), SHOP_BUYPROFIT(shop_nr), SHOP_OPEN1(shop_nr),
	SHOP_CLOSE1(shop_nr), SHOP_OPEN2(shop_nr), SHOP_CLOSE2(shop_nr));


  /* Need a local buffer. */
  {
    char buf1[128];
    sprintbit(SHOP_BITVECTOR(shop_nr), shop_bits, buf1, sizeof(buf1));
    send_to_char(ch, "Bits:       %s\r\n", buf1);
  }
}


void show_shops(struct char_data *ch, char *arg)
{
  int shop_nr;

  if (!*arg)
    list_all_shops(ch);
  else {
    if (!str_cmp(arg, ".")) {
      for (shop_nr = 0; shop_nr <= top_shop; shop_nr++)
	if (ok_shop_room(shop_nr, GET_ROOM_VNUM(IN_ROOM(ch))))
	  break;

      if (shop_nr > top_shop) {
	send_to_char(ch, "This isn't a shop!\r\n");
	return;
      }
    } else if (is_number(arg))
      shop_nr = atoi(arg) - 1;
    else
      shop_nr = -1;

    if (shop_nr < 0 || shop_nr > top_shop) {
      send_to_char(ch, "Illegal shop number.\r\n");
      return;
    }
    list_detailed_shop(ch, shop_nr);
  }
}


void destroy_shops(void)
{
  ssize_t cnt, itr;

  if (!shop_index)
    return;

  for (cnt = 0; cnt <= top_shop; cnt++) {
    if (shop_index[cnt].no_such_item1)
      free(shop_index[cnt].no_such_item1);
    if (shop_index[cnt].no_such_item2)
      free(shop_index[cnt].no_such_item2);
    if (shop_index[cnt].missing_cash1)
      free(shop_index[cnt].missing_cash1);
    if (shop_index[cnt].missing_cash2)
      free(shop_index[cnt].missing_cash2);
    if (shop_index[cnt].do_not_buy)
      free(shop_index[cnt].do_not_buy);
    if (shop_index[cnt].message_buy)
      free(shop_index[cnt].message_buy);
    if (shop_index[cnt].message_sell)
      free(shop_index[cnt].message_sell);
    if (shop_index[cnt].in_room)
      free(shop_index[cnt].in_room);
    if (shop_index[cnt].producing)
      free(shop_index[cnt].producing);

    if (shop_index[cnt].type) {
      for (itr = 0; BUY_TYPE(shop_index[cnt].type[itr]) != NOTHING; itr++)
        if (BUY_WORD(shop_index[cnt].type[itr]))
          free(BUY_WORD(shop_index[cnt].type[itr]));
      free(shop_index[cnt].type);
    }
  }

  free(shop_index);
  shop_index = NULL;
  top_shop = -1;
}
