tbamud/src/roomsave.c

711 lines
21 KiB
C
Raw Normal View History

2025-09-07 08:29:04 -07:00
/**
* @file roomsave.c
* Numeric and string contants used by the MUD.
*
* An addition to the core tbaMUD source code distribution, which is a derivative
* of, and continuation of, CircleMUD.
*
* All rights reserved. See license for complete information.
* Copyright (C) 1993, 94 by the Trustees of the Johns Hopkins University
* CircleMUD is based on DikuMUD, Copyright (C) 1990, 1991.
*
*/
#include "conf.h"
#include "sysdep.h"
#include <stdlib.h>
2025-09-07 08:29:04 -07:00
#include <dirent.h>
#include <errno.h>
#include <limits.h>
#ifndef PATH_MAX
#define PATH_MAX 4096
#endif
#include "structs.h"
#include "utils.h"
#include "db.h"
#include "handler.h"
#include "comm.h"
#include "constants.h"
#include "roomsave.h"
/* Write saved rooms under lib/world/rsv/<vnum>.rsv (like wld/ zon/ obj/). */
#ifndef ROOMSAVE_PREFIX
#define ROOMSAVE_PREFIX LIB_WORLD "rsv/"
#endif
#ifndef ROOMSAVE_EXT
#define ROOMSAVE_EXT ".rsv"
#endif
static unsigned char *roomsave_dirty = NULL;
void RoomSave_init_dirty(void) {
free(roomsave_dirty);
roomsave_dirty = calloc((size_t)top_of_world + 1, 1);
}
void RoomSave_mark_dirty_room(room_rnum rnum) {
if (!roomsave_dirty) return;
if (rnum != NOWHERE && rnum >= 0 && rnum <= top_of_world)
roomsave_dirty[rnum] = 1;
}
/* Where does an object “live” (topmost location -> room)? */
room_rnum RoomSave_room_of_obj(struct obj_data *o) {
if (!o) return NOWHERE;
while (o->in_obj) o = o->in_obj;
if (o->carried_by) return IN_ROOM(o->carried_by);
if (o->worn_by) return IN_ROOM(o->worn_by);
return o->in_room;
}
2025-09-07 08:29:04 -07:00
/* --- helper: read a list of objects until '.' or 'E' and return the head --- */
/* Context-aware implementation: stop_on_E = 1 for nested B..E, 0 for top-level. */
static struct obj_data *roomsave_read_list_ctx(FILE *fl, int stop_on_E)
{
2025-09-07 08:29:04 -07:00
char line[256];
struct obj_data *head = NULL, *tail = NULL;
while (fgets(line, sizeof(line), fl)) {
if (line[0] == '.') {
2025-09-07 08:29:04 -07:00
/* End of this list scope */
break;
}
if (stop_on_E && line[0] == 'E') {
/* End of nested (B..E) scope */
break;
}
/* For top-level reads (stop_on_E==0), or any non-'O', push back
so the outer #R reader can handle M/E/G/P or '.' */
if (line[0] != 'O') {
long back = -((long)strlen(line));
fseek(fl, back, SEEK_CUR);
break;
}
2025-09-07 08:29:04 -07:00
2025-12-24 08:38:38 -08:00
/* Parse object header: O vnum timer weight cost unused */
int vnum, timer, weight, cost, unused_cost;
if (sscanf(line, "O %d %d %d %d %d", &vnum, &timer, &weight, &cost, &unused_cost) != 5)
2025-09-07 08:29:04 -07:00
continue;
/* IMPORTANT: read by VNUM (VIRTUAL), not real index */
struct obj_data *obj = read_object((obj_vnum)vnum, VIRTUAL);
if (!obj) {
mudlog(NRM, LVL_IMMORT, TRUE, "RoomSave: read_object(vnum=%d) failed.", vnum);
/* Skip to next object/header or end-of-scope */
long backpos;
while (fgets(line, sizeof(line), fl)) {
if (line[0] == 'O' || line[0] == '.' || (stop_on_E && line[0] == 'E')) {
2025-09-07 08:29:04 -07:00
backpos = -((long)strlen(line));
fseek(fl, backpos, SEEK_CUR);
break;
}
}
continue;
}
/* Apply core scalars */
GET_OBJ_TIMER(obj) = timer;
GET_OBJ_WEIGHT(obj) = weight;
GET_OBJ_COST(obj) = cost;
2025-12-24 08:38:38 -08:00
GET_OBJ_COST_PER_DAY(obj) = 0;
2025-09-07 08:29:04 -07:00
/* Clear array flags so missing slots don't keep proto bits */
#ifdef EF_ARRAY_MAX
# ifdef GET_OBJ_EXTRA_AR
for (int i = 0; i < EF_ARRAY_MAX; i++) GET_OBJ_EXTRA_AR(obj, i) = 0;
# else
for (int i = 0; i < EF_ARRAY_MAX; i++) GET_OBJ_EXTRA(obj)[i] = 0;
# endif
#endif
#ifdef TW_ARRAY_MAX
for (int i = 0; i < TW_ARRAY_MAX; i++) GET_OBJ_WEAR(obj)[i] = 0;
#endif
/* Read per-object lines until next 'O' or '.' or 'E'(when nested) */
2025-09-07 08:29:04 -07:00
long backpos;
while (fgets(line, sizeof(line), fl)) {
if (line[0] == 'V') {
int idx, val;
if (sscanf(line, "V %d %d", &idx, &val) == 2) {
#ifdef NUM_OBJ_VAL_POSITIONS
if (idx >= 0 && idx < NUM_OBJ_VAL_POSITIONS) GET_OBJ_VAL(obj, idx) = val;
#else
if (idx >= 0 && idx < 6) GET_OBJ_VAL(obj, idx) = val;
#endif
}
continue;
} else if (line[0] == 'X') { /* extra flags */
int idx, val;
if (sscanf(line, "X %d %d", &idx, &val) == 2) {
#if defined(EF_ARRAY_MAX) && defined(GET_OBJ_EXTRA_AR)
if (idx >= 0 && idx < EF_ARRAY_MAX) GET_OBJ_EXTRA_AR(obj, idx) = val;
#elif defined(EF_ARRAY_MAX)
if (idx >= 0 && idx < EF_ARRAY_MAX) GET_OBJ_EXTRA(obj)[idx] = val;
#else
if (idx == 0) GET_OBJ_EXTRA(obj) = val;
#endif
}
continue;
} else if (line[0] == 'W') { /* wear flags */
int idx, val;
if (sscanf(line, "W %d %d", &idx, &val) == 2) {
#ifdef TW_ARRAY_MAX
if (idx >= 0 && idx < TW_ARRAY_MAX) GET_OBJ_WEAR(obj)[idx] = val;
#else
if (idx == 0) GET_OBJ_WEAR(obj) = val;
#endif
}
continue;
} else if (line[0] == 'B') {
/* Nested contents until matching 'E' */
struct obj_data *child_head = roomsave_read_list_ctx(fl, 1 /* stop_on_E */);
2025-09-07 08:29:04 -07:00
/* Detach each node before obj_to_obj(), otherwise we lose siblings */
2025-09-07 08:29:04 -07:00
for (struct obj_data *it = child_head, *next; it; it = next) {
next = it->next_content; /* remember original sibling */
it->next_content = NULL; /* detach from temp list */
obj_to_obj(it, obj); /* push into container (LIFO) */
}
continue;
} else if (line[0] == 'O' || line[0] == '.' || (stop_on_E && line[0] == 'E')) {
2025-09-07 08:29:04 -07:00
/* Next object / end-of-scope: rewind one line for outer loop to see it */
backpos = -((long)strlen(line));
fseek(fl, backpos, SEEK_CUR);
break;
} else {
/* ignore unknown lines */
2025-09-07 08:29:04 -07:00
continue;
}
}
/* Append to this scope's list */
obj->next_content = NULL;
if (!head) head = tail = obj;
else { tail->next_content = obj; tail = obj; }
}
return head;
}
/* Keep your existing one-arg API for callers: top-level semantics (stop_on_E = 0). */
static struct obj_data *roomsave_read_list(FILE *fl)
{
return roomsave_read_list_ctx(fl, 0);
}
2025-09-07 08:29:04 -07:00
/* ---------- Minimal line format ----------
#R <vnum> <unix_time>
2025-12-24 08:38:38 -08:00
O <vnum> <timer> <extra_flags> <wear_flags> <weight> <cost> <unused>
2025-09-07 08:29:04 -07:00
V <i> <val[i]> ; repeated for all value slots present on this obj
B ; begin contents of this object (container)
E ; end contents of this object
. ; end of room
------------------------------------------ */
static void ensure_dir_exists(const char *path) {
if (mkdir(path, 0775) == -1 && errno != EEXIST) {
mudlog(CMP, LVL_IMMORT, TRUE, "SYSERR: roomsave mkdir(%s): %s", path, strerror(errno));
}
}
/* zone vnum for a given room rnum (e.g., 134 -> zone 1) */
static int roomsave_zone_for_rnum(room_rnum rnum) {
if (rnum == NOWHERE || rnum < 0 || rnum > top_of_world) return 0;
zone_rnum znum = world[rnum].zone;
if (znum < 0 || znum > top_of_zone_table) return 0;
return zone_table[znum].number; /* e.g., 1 for rooms 100199, 2 for 200299, etc. */
2025-09-07 08:29:04 -07:00
}
/* lib/world/rsv/<zone>.rsv */
static void roomsave_zone_filename(int zone_vnum, char *out, size_t outsz) {
snprintf(out, outsz, "%s%d%s", ROOMSAVE_PREFIX, zone_vnum, ROOMSAVE_EXT);
}
/* Write one object (and its recursive contents) */
static void write_one_object(FILE *fl, struct obj_data *obj) {
int i;
/* Core scalars (flags printed separately per-slot) */
fprintf(fl, "O %d %d %d %d %d\n",
GET_OBJ_VNUM(obj),
GET_OBJ_TIMER(obj),
GET_OBJ_WEIGHT(obj),
GET_OBJ_COST(obj),
2025-12-24 08:38:38 -08:00
GET_OBJ_COST_PER_DAY(obj));
2025-09-07 08:29:04 -07:00
/* Extra flags array */
#if defined(EF_ARRAY_MAX) && defined(GET_OBJ_EXTRA_AR)
for (i = 0; i < EF_ARRAY_MAX; i++)
fprintf(fl, "X %d %d\n", i, GET_OBJ_EXTRA_AR(obj, i));
#elif defined(EF_ARRAY_MAX)
for (i = 0; i < EF_ARRAY_MAX; i++)
fprintf(fl, "X %d %d\n", i, GET_OBJ_EXTRA(obj)[i]);
#else
fprintf(fl, "X %d %d\n", 0, GET_OBJ_EXTRA(obj));
#endif
/* Wear flags array */
#ifdef TW_ARRAY_MAX
for (i = 0; i < TW_ARRAY_MAX; i++)
fprintf(fl, "W %d %d\n", i, GET_OBJ_WEAR(obj)[i]);
#else
fprintf(fl, "W %d %d\n", 0, GET_OBJ_WEAR(obj));
#endif
/* Values[] (durability, liquids, charges, etc.) */
#ifdef NUM_OBJ_VAL_POSITIONS
for (i = 0; i < NUM_OBJ_VAL_POSITIONS; i++)
fprintf(fl, "V %d %d\n", i, GET_OBJ_VAL(obj, i));
#else
for (i = 0; i < 6; i++)
fprintf(fl, "V %d %d\n", i, GET_OBJ_VAL(obj, i));
#endif
/* Contents (recursive) */
if (obj->contains) {
struct obj_data *cont;
fprintf(fl, "B\n");
for (cont = obj->contains; cont; cont = cont->next_content)
write_one_object(fl, cont);
fprintf(fl, "E\n");
}
}
/* Forward declaration for RoomSave_now*/
static void RS_write_room_mobs(FILE *out, room_rnum rnum);
2025-09-07 08:29:04 -07:00
/* Public: write the entire rooms contents */
int RoomSave_now(room_rnum rnum) {
char path[PATH_MAX], tmp[PATH_MAX], line[512];
FILE *in = NULL, *out = NULL;
room_vnum rvnum;
int zvnum;
if (rnum == NOWHERE)
return 0;
rvnum = world[rnum].number;
zvnum = roomsave_zone_for_rnum(rnum);
if (zvnum < 0)
return 0;
ensure_dir_exists(ROOMSAVE_PREFIX);
roomsave_zone_filename(zvnum, path, sizeof(path));
{
int n = snprintf(tmp, sizeof(tmp), "%s.tmp", path);
if (n < 0 || n >= (int)sizeof(tmp)) {
2025-10-04 09:42:52 -07:00
mudlog(NRM, LVL_IMMORT, TRUE,
"SYSERR: RoomSave: temp path too long for %s", path);
2025-09-07 08:29:04 -07:00
return 0;
}
}
if (!(out = fopen(tmp, "w"))) {
2025-10-04 09:42:52 -07:00
mudlog(NRM, LVL_IMMORT, TRUE,
"SYSERR: RoomSave: fopen(%s) failed: %s",
tmp, strerror(errno));
2025-09-07 08:29:04 -07:00
return 0;
}
if ((in = fopen(path, "r")) != NULL) {
while (fgets(line, sizeof(line), in)) {
if (strncmp(line, "#R ", 3) == 0) {
int file_rvnum;
long ts;
if (sscanf(line, "#R %d %ld", &file_rvnum, &ts) == 2) {
if (file_rvnum == (int)rvnum) {
2025-10-04 09:42:52 -07:00
/* Skip old block completely until and including '.' line */
2025-09-07 08:29:04 -07:00
while (fgets(line, sizeof(line), in)) {
2025-10-04 09:42:52 -07:00
if (line[0] == '.') {
/* consume it and break */
2025-09-07 08:29:04 -07:00
break;
2025-10-04 09:42:52 -07:00
}
2025-09-07 08:29:04 -07:00
}
continue; /* do NOT write skipped lines */
}
}
}
2025-10-04 09:42:52 -07:00
fputs(line, out); /* keep unrelated lines */
2025-09-07 08:29:04 -07:00
}
fclose(in);
}
2025-10-04 09:42:52 -07:00
/* Append new block */
2025-09-07 08:29:04 -07:00
fprintf(out, "#R %d %ld\n", rvnum, (long)time(0));
RS_write_room_mobs(out, rnum);
2025-09-07 08:29:04 -07:00
for (struct obj_data *obj = world[rnum].contents; obj; obj = obj->next_content)
write_one_object(out, obj);
2025-10-04 09:42:52 -07:00
/* Always terminate block */
2025-09-07 08:29:04 -07:00
fprintf(out, ".\n");
if (fclose(out) != 0) {
2025-10-04 09:42:52 -07:00
mudlog(NRM, LVL_IMMORT, TRUE,
"SYSERR: RoomSave: fclose(%s) failed: %s",
tmp, strerror(errno));
2025-09-07 08:29:04 -07:00
return 0;
}
if (rename(tmp, path) != 0) {
2025-10-04 09:42:52 -07:00
mudlog(NRM, LVL_IMMORT, TRUE,
"SYSERR: RoomSave: rename(%s -> %s) failed: %s",
tmp, path, strerror(errno));
2025-09-07 08:29:04 -07:00
return 0;
}
return 1;
}
/* --- M/E/G/P load helpers (mob restore) -------------------------------- */
struct rs_load_ctx {
room_rnum rnum;
struct char_data *cur_mob; /* last mob spawned by 'M' */
struct obj_data *stack[16]; /* container stack by depth for 'P' */
};
static struct obj_data *RS_create_obj_by_vnum(obj_vnum ov) {
obj_rnum ornum;
if (ov <= 0) return NULL;
ornum = real_object(ov);
if (ornum == NOTHING) return NULL;
return read_object(ornum, REAL);
}
static struct char_data *RS_create_mob_by_vnum(mob_vnum mv) {
mob_rnum mrnum;
if (mv <= 0) return NULL;
mrnum = real_mobile(mv);
if (mrnum == NOBODY) return NULL;
return read_mobile(mrnum, REAL);
}
2025-10-04 09:42:52 -07:00
/* Reset the loader context before reading a new #R block */
static void RS_ctx_clear(struct rs_load_ctx *ctx) {
if (!ctx)
return;
/* DO NOT reset ctx->rnum — each #R block sets this explicitly
* before parsing mobs or objects. Resetting it causes cross-room
* bleed (e.g., mobs from one room spawning in another).
*/
ctx->cur_mob = NULL;
/* Clear all container stack pointers */
for (int i = 0; i < 16; i++)
ctx->stack[i] = NULL;
}
/* Optional autosave hook (invoked by limits.c:point_update). */
void RoomSave_autosave_tick(void) {
/* Iterate all rooms; only save flagged ones. */
for (room_rnum rnum = 0; rnum <= top_of_world; ++rnum) {
if (ROOM_FLAGGED(rnum, ROOM_SAVE))
RoomSave_now(rnum);
}
}
/* Forward decl so RS_parse_mob_line can use it without implicit declaration */
static void RS_stack_clear(struct rs_load_ctx *ctx);
/* Handle one line inside a #R block. Returns 1 if handled here. */
static int RS_parse_mob_line(struct rs_load_ctx *ctx, char *line)
{
if (!line) return 0;
while (*line == ' ' || *line == '\t') ++line;
if (!*line) return 0;
switch (line[0]) {
2025-10-03 11:23:04 -07:00
case 'M': {
mob_vnum mv;
if (sscanf(line+1, " %d", (int *)&mv) != 1) return 0;
ctx->cur_mob = RS_create_mob_by_vnum(mv);
2025-10-03 11:23:04 -07:00
if (!ctx->cur_mob) return 1;
/* Place in the block's room */
char_to_room(ctx->cur_mob, ctx->rnum);
2025-10-03 11:23:04 -07:00
/* Safety: if anything put it elsewhere, force it back */
if (IN_ROOM(ctx->cur_mob) != ctx->rnum)
char_to_room(ctx->cur_mob, ctx->rnum);
RS_stack_clear(ctx); /* clear only container stack */
return 1;
}
case 'E': { /* E <wear_pos> <obj_vnum> */
int pos; obj_vnum ov; struct obj_data *obj;
if (!ctx->cur_mob) return 1; /* orphan -> ignore */
if (sscanf(line+1, " %d %d", &pos, (int *)&ov) != 2) return 0;
obj = RS_create_obj_by_vnum(ov);
if (!obj) return 1;
if (pos < 0 || pos >= NUM_WEARS) pos = WEAR_HOLD; /* clamp */
equip_char(ctx->cur_mob, obj, pos);
/* Reset ONLY container stack for following P-lines; keep cur_mob */
RS_stack_clear(ctx);
ctx->stack[0] = obj;
return 1;
}
case 'G': { /* G <obj_vnum> */
obj_vnum ov; struct obj_data *obj;
if (!ctx->cur_mob) return 1; /* orphan -> ignore */
if (sscanf(line+1, " %d", (int *)&ov) != 1) return 0;
obj = RS_create_obj_by_vnum(ov);
if (!obj) return 1;
obj_to_char(obj, ctx->cur_mob);
RS_stack_clear(ctx);
ctx->stack[0] = obj;
return 1;
}
case 'P': { /* P <depth> <obj_vnum> : put into last obj at (depth-1) */
int depth; obj_vnum ov; struct obj_data *parent, *obj;
if (sscanf(line+1, " %d %d", &depth, (int *)&ov) != 2) return 0;
if (depth <= 0 || depth >= (int)(sizeof(ctx->stack)/sizeof(ctx->stack[0])))
return 1;
parent = ctx->stack[depth-1];
if (!parent) return 1;
obj = RS_create_obj_by_vnum(ov);
if (!obj) return 1;
obj_to_obj(obj, parent);
ctx->stack[depth] = obj;
{ int d; for (d = depth+1; d < (int)(sizeof(ctx->stack)/sizeof(ctx->stack[0])); ++d) ctx->stack[d] = NULL; }
return 1;
}
default:
return 0;
}
}
/* Forward decls for mob restore helpers */
static void RS_stack_clear(struct rs_load_ctx *ctx);
void RoomSave_boot(void)
{
2025-09-07 08:29:04 -07:00
DIR *dirp;
struct dirent *dp;
ensure_dir_exists(ROOMSAVE_PREFIX);
dirp = opendir(ROOMSAVE_PREFIX);
if (!dirp) {
2025-10-04 09:42:52 -07:00
mudlog(NRM, LVL_IMMORT, TRUE,
"SYSERR: RoomSave_boot: cannot open %s", ROOMSAVE_PREFIX);
2025-09-07 08:29:04 -07:00
return;
}
log("RoomSave: scanning %s for *.rsv", ROOMSAVE_PREFIX);
while ((dp = readdir(dirp))) {
size_t n = strlen(dp->d_name);
if (n < 5) continue; /* skip . and .. */
2025-09-07 08:29:04 -07:00
if (strcmp(dp->d_name + n - 4, ROOMSAVE_EXT) != 0) continue;
{
2025-10-04 09:42:52 -07:00
char path[PATH_MAX];
int wn = snprintf(path, sizeof(path), "%s%s", ROOMSAVE_PREFIX, dp->d_name);
if (wn < 0 || wn >= (int)sizeof(path)) {
mudlog(NRM, LVL_IMMORT, TRUE,
"SYSERR: RoomSave_boot: path too long: %s%s",
ROOMSAVE_PREFIX, dp->d_name);
continue;
}
2025-09-07 08:29:04 -07:00
2025-10-04 09:42:52 -07:00
FILE *fl = fopen(path, "r");
if (!fl) {
2025-10-03 11:23:04 -07:00
mudlog(NRM, LVL_IMMORT, TRUE,
2025-10-04 09:42:52 -07:00
"SYSERR: RoomSave_boot: fopen(%s) failed: %s",
path, strerror(errno));
continue;
}
2025-09-07 08:29:04 -07:00
2025-10-04 09:42:52 -07:00
log("RoomSave: reading %s", path);
2025-09-07 08:29:04 -07:00
2025-10-04 09:42:52 -07:00
int blocks = 0;
int restored_objs_total = 0;
int restored_mobs_total = 0;
2025-09-07 08:29:04 -07:00
2025-10-04 09:42:52 -07:00
/* Outer loop: read every #R block in this .rsv file */
char line[512];
while (fgets(line, sizeof(line), fl)) {
2025-09-07 08:29:04 -07:00
2025-10-04 09:42:52 -07:00
/* Skip until a valid #R header */
if (strncmp(line, "#R ", 3) != 0)
continue;
2025-09-07 08:29:04 -07:00
2025-10-04 09:42:52 -07:00
/* Parse header line */
int rvnum; long ts;
if (sscanf(line, "#R %d %ld", &rvnum, &ts) != 2) {
mudlog(NRM, LVL_IMMORT, TRUE,
"RoomSave: malformed #R header in %s: %s", path, line);
/* Skip malformed block */
while (fgets(line, sizeof(line), fl))
if (line[0] == '.') break;
continue;
2025-10-03 11:23:04 -07:00
}
2025-10-04 09:42:52 -07:00
blocks++;
/* Resolve the room for this block */
room_rnum rnum = real_room((room_vnum)rvnum);
if (rnum == NOWHERE) {
mudlog(NRM, LVL_IMMORT, FALSE,
"RoomSave: unknown room vnum %d in %s (skipping)",
rvnum, path);
/* Skip to next block */
while (fgets(line, sizeof(line), fl))
if (line[0] == '.') break;
2025-10-03 11:23:04 -07:00
continue;
}
2025-09-07 08:29:04 -07:00
2025-10-04 09:42:52 -07:00
/* Clear this room's ground contents before restoring */
while (world[rnum].contents)
extract_obj(world[rnum].contents);
/* Clear and set mob context for this block */
struct rs_load_ctx mctx;
RS_ctx_clear(&mctx);
2025-10-03 11:23:04 -07:00
mctx.rnum = rnum;
2025-10-04 09:42:52 -07:00
/* Per-block counts */
int count_objs = 0, count_mobs = 0;
char inner[512];
/* Inner loop: read this #R block until '.' */
while (fgets(inner, sizeof(inner), fl)) {
/* Trim spaces */
while (inner[0] == ' ' || inner[0] == '\t')
memmove(inner, inner + 1, strlen(inner));
/* Stop at end of block */
if (inner[0] == '.')
break;
/* Defensive: stop if another #R starts (malformed file) */
if (!strncmp(inner, "#R ", 3)) {
fseek(fl, -((long)strlen(inner)), SEEK_CUR);
break;
}
2025-10-04 09:42:52 -07:00
/* Handle object blocks */
if (inner[0] == 'O') {
long pos = ftell(fl);
fseek(fl, pos - strlen(inner), SEEK_SET);
struct obj_data *list = roomsave_read_list(fl);
for (struct obj_data *it = list, *next; it; it = next) {
next = it->next_content;
it->next_content = NULL;
obj_to_room(it, rnum);
count_objs++;
}
continue;
}
/* Handle mob & equipment/inventory */
if (RS_parse_mob_line(&mctx, inner)) {
if (inner[0] == 'M')
count_mobs++;
continue;
}
/* Unknown token: ignore gracefully */
2025-09-07 08:29:04 -07:00
}
2025-10-03 11:23:04 -07:00
2025-10-04 09:42:52 -07:00
restored_objs_total += count_objs;
restored_mobs_total += count_mobs;
if (count_mobs > 0)
log("RoomSave: room %d <- %d object(s) and %d mob(s)",
rvnum, count_objs, count_mobs);
else
log("RoomSave: room %d <- %d object(s)", rvnum, count_objs);
2025-09-07 08:29:04 -07:00
}
2025-10-04 09:42:52 -07:00
log("RoomSave: finished %s (blocks=%d, objects=%d, mobs=%d)",
path, blocks, restored_objs_total, restored_mobs_total);
2025-10-04 09:42:52 -07:00
fclose(fl);
}
2025-09-07 08:29:04 -07:00
}
closedir(dirp);
}
/* ======== MOB SAVE: write NPCs and their equipment/inventory ========== */
/* Depth-aware writer for container contents under a parent object.
* Writes: P <depth> <obj_vnum>
* depth starts at 1 for direct children. */
static void RS_write_P_chain(FILE *fp, struct obj_data *parent, int depth) {
struct obj_data *c;
for (c = parent->contains; c; c = c->next_content) {
obj_vnum cv = GET_OBJ_VNUM(c);
if (cv <= 0) continue; /* skip non-proto / invalid */
fprintf(fp, "P %d %d\n", depth, (int)cv);
if (c->contains)
RS_write_P_chain(fp, c, depth + 1);
}
}
/* Writes: E <wear_pos> <obj_vnum> (then P-chain) */
static void RS_write_mob_equipment(FILE *fp, struct char_data *mob) {
int w;
for (w = 0; w < NUM_WEARS; ++w) {
struct obj_data *eq = GET_EQ(mob, w);
if (!eq) continue;
if (GET_OBJ_VNUM(eq) <= 0) continue;
fprintf(fp, "E %d %d\n", w, (int)GET_OBJ_VNUM(eq));
if (eq->contains) RS_write_P_chain(fp, eq, 1);
}
}
/* Writes: G <obj_vnum> for inventory items (then P-chain) */
static void RS_write_mob_inventory(FILE *fp, struct char_data *mob) {
struct obj_data *o;
for (o = mob->carrying; o; o = o->next_content) {
if (GET_OBJ_VNUM(o) <= 0) continue;
fprintf(fp, "G %d\n", (int)GET_OBJ_VNUM(o));
if (o->contains) RS_write_P_chain(fp, o, 1);
}
}
/* Top-level writer: for each NPC in room, emit:
* M <mob_vnum>
* [E ...]*
* [G ...]*
* (Players are ignored.) */
static void RS_write_room_mobs(FILE *out, room_rnum rnum) {
struct char_data *mob;
for (mob = world[rnum].people; mob; mob = mob->next_in_room) {
if (!IS_NPC(mob)) continue;
if (GET_MOB_VNUM(mob) <= 0) continue;
fprintf(out, "M %d\n", (int)GET_MOB_VNUM(mob));
RS_write_mob_equipment(out, mob);
RS_write_mob_inventory(out, mob);
}
}
/* Clear only the container stack, NOT cur_mob */
static void RS_stack_clear(struct rs_load_ctx *ctx) {
int i;
for (i = 0; i < (int)(sizeof(ctx->stack)/sizeof(ctx->stack[0])); ++i)
ctx->stack[i] = NULL;
}