Message
| For the fun of it, I wrote a small Lua utility to analyze the new format (keyed) SMAUG area files.
What I mean by "keyed" is that entries now look like this:
#MOBILE
Vnum 10300
Keywords wolf arena~
Short the dread wolf~
Long A dread wolf is hunting here
~
Race human~
Class warrior~
Position standing~
DefPos standing~
Gender neuter~
Actflags npc stayarea prototype~
Stats1 0 4 0 35 500 0
Stats2 44 0 1
Stats3 0 0 0
Stats4 0 0 0 0 0
Attribs 13 13 13 13 13 13 13
Saves 0 0 0 0 0
Speaks common~
Speaking common~
Bodyparts legs feet eye tail~
Immune charm~
#ENDMOBILE
This lends itself somewhat to a simple utility to read the whole area file in. I ran this using the stand-alone Lua executable, however it also works from the MUSHclient "immediate" window with Lua scripting. To use it you would need to change the first line to represent where your Smaug "area" directory is on your hard disk.
local AREA_DIR = "C:\\cygwin\\home\\Owner\\smaugfuss\\area\\"
local hdr -- area header
local mobs = {} -- all mobs
local objects = {} -- all objects
local rooms = {} -- all rooms
local helps = {} -- help stuff
require "tprint"
require "pairsbykeys"
-- extract keyed data, items in "strings" are tilde-terminated
function extract_stuff (src, strings)
local tbl = {}
-- note: we expect \n to be at each end of src
-- strings might be multi-line, and are terminated by the ~ character
for key in string.gmatch (strings, "[%a%d]+") do
src = string.gsub (src, "(" .. key .. ")%s+(.-)~",
function (k, v)
assert (not tbl [k], "Duplicate string item: " .. k .. " " .. v) -- should not already exist
tbl [k] = v
return ""
end -- function
)
end -- for all strings
-- everything else is single line, terminated by \n
src = string.gsub (src, "([%a%d]+)%s+(.-)\n",
function (k, v)
assert (not tbl [k], "Duplicate item: " .. k .. " " .. v) -- should not already exist
tbl [k] = v
return ""
end -- function
)
-- should have nothing left, if not, we have some sort of problem
if (string.match (src, "[^%s]")) then
tprint (tbl)
error ("Got extra data:" .. src)
end -- if
return tbl
end -- extract_stuff
-- area header has stuff like author, reset message
function process_area_header (ah)
hdr = extract_stuff (ah, "Name Author ResetMsg Flags")
end -- process_area_header
-- do each mobile
function process_mobile (m)
local mudprogs = {}
-- first pull out the mud progs sub-items
m = string.gsub (m, "#MUDPROG(\n.-\n)#ENDPROG\n",
function (p)
table.insert (mudprogs, extract_stuff (p, "Progtype Arglist Comlist"))
return ""
end -- function
)
local mob = extract_stuff (m,
"Keywords Short Long Race Class Position DefPos Gender Actflags Speaks Speaking Bodyparts Immune Desc Defenses Attacks")
if next (mudprogs) ~= nil then
mob.mudprogs = mudprogs
end -- have some mud progs
local vnum = assert (tonumber (mob.Vnum))
assert (not mobs [vnum], "Duplicate mob vnum: " .. vnum) -- should not already exist
mobs [vnum] = mob
end -- process_mobile
-- do each object
function process_object (m)
local mudprogs = {}
local extra_descs = {}
local affects = {}
-- first pull out the mud progs sub-items
m = string.gsub (m, "#MUDPROG(\n.-\n)#ENDPROG\n",
function (p)
table.insert (mudprogs, extract_stuff (p, "Progtype Arglist Comlist"))
return "\n"
end -- function
)
-- then pull out the extra description sub-items
m = string.gsub (m, "#EXDESC(\n.-\n)#ENDEXDESC\n",
function (p)
table.insert (extra_descs, extract_stuff (p, "ExDescKey ExDesc"))
return "\n"
end -- function
)
-- and the affects of which there might be more than one
m = string.gsub (m, "Affect%s+(.-)\n",
function (p)
table.insert (affects, p)
return "\n"
end -- function
)
local object = extract_stuff (m,
"Keywords Short Long Type WFlags Flags")
if next (mudprogs) ~= nil then
object.mudprogs = mudprogs
end -- have some mud progs
if next (extra_descs) ~= nil then
object.extra_descs = extra_descs
end -- have some extra descriptions
if next (affects) ~= nil then
object.affects = affects
end -- have some affects
local vnum = assert (tonumber (object.Vnum))
assert (not objects [vnum], "Duplicate object vnum: " .. vnum) -- should not already exist
objects [vnum] = object
end -- process_object
-- do each room
function process_room (m)
local mudprogs = {}
local exits = {}
local resets = {}
local exit_descs = {}
-- first pull out the mud progs sub-items
m = string.gsub (m, "#MUDPROG(\n.-\n)#ENDPROG\n",
function (p)
table.insert (mudprogs, extract_stuff (p, "Progtype Arglist Comlist"))
return ""
end -- function
)
-- then pull out the exits
m = string.gsub (m, "#EXIT(\n.-\n)#ENDEXIT\n",
function (p)
table.insert (exits, extract_stuff (p, "Direction Desc"))
return ""
end -- function
)
-- then pull out the exit descriptions
m = string.gsub (m, "#EXDESC(\n.-\n)#ENDEXDESC\n",
function (p)
table.insert (exit_descs, extract_stuff (p, "ExDescKey ExDesc"))
return ""
end -- function
)
-- and the resets of which there might be more than one
m = string.gsub (m, "Reset%s+(.-)\n",
function (p)
table.insert (resets, p)
return "\n"
end -- function
)
local room = extract_stuff (m,
"Name Sector Flags Desc", {})
if next (mudprogs) ~= nil then
room.mudprogs = mudprogs
end -- have some mud progs
if next (exits) ~= nil then
room.exits = exits
end -- have some exits
if next (exit_descs) ~= nil then
room.exit_descs = exit_descs
end -- have some exits descriptions
if next (resets) ~= nil then
room.resets = resets
end -- have some resets
local vnum = assert (tonumber (room.Vnum))
assert (not rooms [vnum], "Duplicate room vnum: " .. vnum) -- should not already exist
rooms [vnum] = room
end -- process_room
-- areas consist of area header, mobiles, objects, rooms
function process_area (areadata)
local m
-- process header stuff
for m in string.gmatch (areadata, "#AREADATA(\n.-\n)#ENDAREADATA\n") do
process_area_header (m)
end -- for
-- now mobs
for m in string.gmatch (areadata, "#MOBILE(\n.-\n)#ENDMOBILE\n") do
process_mobile (m)
end -- for
-- now objects
for m in string.gmatch (areadata, "#OBJECT(\n.-\n)#ENDOBJECT\n") do
process_object (m)
end -- for
-- now rooms
for m in string.gmatch (areadata, "#ROOM(\n.-\n)#ENDROOM\n") do
process_room (m)
end -- for
end -- process_area
-- handle all help stuff in help.are
function process_helps (areadata)
string.gsub (areadata, "%s*(%d+)%s+(.-)~\n(.-)~",
function (level, keyword, text)
table.insert (helps, { level = tonumber (level), keyword = keyword, text = text } )
end -- function
)
end -- process_helps
-- help function to show count of items in a table
function show_counts (tbl, desc)
local count = 0
for k in pairs (tbl) do
count = count + 1
end -- for
print (count, desc)
end -- show_counts
-------- MAIN PROCESSING STARTS HERE --------------------
-- read each line in area.lst file
for line in io.lines (AREA_DIR .. "area.lst") do
-- end of file marker
if line == "$" then
break
end -- if
-- area file name
local filename = AREA_DIR .. line
-- read area in
local f = io.open (filename, "rt")
local area = f:read ("*a")
f:close ()
-- show name and size
print ("Opened " .. filename .. ", got " .. #area .. " bytes")
-- process entire area
for a in string.gmatch (area, "#FUSSAREA(\n.-\n)#ENDAREA\n") do
process_area (a)
end -- for
-- do helps
for a in string.gmatch (area, "#HELPS(\n.-\n)#$\n") do
process_helps (a)
end -- for
end
-- show how many we found
show_counts (mobs, "mobs")
show_counts (objects, "objects")
show_counts (rooms, "rooms")
show_counts (helps, "helps")
It uses the area.lst file to find which area files to process, and reads the lot in. On my PC it took 2 seconds to process all 24 area files.
Running it, I see this:
Opened C:\cygwin\home\Owner\smaugfuss\area\help.are, got 602742 bytes
Opened C:\cygwin\home\Owner\smaugfuss\area\limbo.are, got 102567 bytes
Opened C:\cygwin\home\Owner\smaugfuss\area\gods.are, got 2202 bytes
Opened C:\cygwin\home\Owner\smaugfuss\area\newacad.are, got 233672 bytes
Opened C:\cygwin\home\Owner\smaugfuss\area\newgate.are, got 66917 bytes
Opened C:\cygwin\home\Owner\smaugfuss\area\newdark.are, got 256634 bytes
Opened C:\cygwin\home\Owner\smaugfuss\area\plains.are, got 38843 bytes
Opened C:\cygwin\home\Owner\smaugfuss\area\haon.are, got 100273 bytes
Opened C:\cygwin\home\Owner\smaugfuss\area\midennir.are, got 70083 bytes
Opened C:\cygwin\home\Owner\smaugfuss\area\sewer.are, got 139946 bytes
Opened C:\cygwin\home\Owner\smaugfuss\area\redferne.are, got 16883 bytes
Opened C:\cygwin\home\Owner\smaugfuss\area\grove.are, got 32229 bytes
Opened C:\cygwin\home\Owner\smaugfuss\area\dwarven.are, got 35524 bytes
Opened C:\cygwin\home\Owner\smaugfuss\area\daycare.are, got 32186 bytes
Opened C:\cygwin\home\Owner\smaugfuss\area\grave.are, got 26812 bytes
Opened C:\cygwin\home\Owner\smaugfuss\area\chapel.are, got 57517 bytes
Opened C:\cygwin\home\Owner\smaugfuss\area\astral.are, got 113452 bytes
Opened C:\cygwin\home\Owner\smaugfuss\area\Build.are, got 43819 bytes
Opened C:\cygwin\home\Owner\smaugfuss\area\pixie.are, got 21871 bytes
Opened C:\cygwin\home\Owner\smaugfuss\area\export.are, got 101487 bytes
Opened C:\cygwin\home\Owner\smaugfuss\area\srefuge.are, got 148135 bytes
Opened C:\cygwin\home\Owner\smaugfuss\area\manor.are, got 107487 bytes
Opened C:\cygwin\home\Owner\smaugfuss\area\unholy.are, got 104256 bytes
Opened C:\cygwin\home\Owner\smaugfuss\area\gallery.are, got 134695 bytes
514 mobs
741 objects
1812 rooms
1091 helps
Each "type" of data is built into its own Lua table, so you can then analyze them (eg. all mobs into the mobs table).
With the tables built you can then start analyzing your areas. For example, by adding this to the bottom of the script:
------------- EXITS CROSS-REFERENCE ----------------------
-- look for exits cross-reference
for room_vnum, room_data in pairs (rooms) do
-- if room has exits, add them to the target room
if room_data.exits then
for _, ex in ipairs (room_data.exits) do
local room = tonumber (ex.ToRoom)
if rooms [room] then -- to room might be out of area
rooms [room].from_room = rooms [room].from_room or {} -- make table if first time
rooms [room].from_room [room_vnum] = true -- there is an exit from room_vnum to room
else
print ("Room ", room_vnum, "leads to non-existent room", room)
end -- room exists
end -- for each exit
end -- some exits
end -- for each room
-- now show rooms that have no room leading to them
no_exit = {}
for k, v in pairs (rooms) do
if not v.from_room then
table.insert (no_exit, k)
end -- if no exit to here
end -- for each room
-- show rooms in order
table.sort (no_exit)
for _, v in ipairs (no_exit) do
print ("No exit to room", v)
end -- for
This code checks each room to see if an exit leads to it.
My results (this is from the standard SmaugFuss 1.9 distribution) was:
No exit to room 2
No exit to room 3
No exit to room 4
No exit to room 6
No exit to room 8
No exit to room 9
No exit to room 10
No exit to room 11
No exit to room 12
No exit to room 13
No exit to room 14
No exit to room 15
No exit to room 21
No exit to room 23
No exit to room 26
No exit to room 27
No exit to room 41
No exit to room 82
No exit to room 105
No exit to room 107
No exit to room 199
No exit to room 878
No exit to room 899
No exit to room 1200
No exit to room 1201
No exit to room 2485
No exit to room 2493
No exit to room 2494
No exit to room 2495
No exit to room 2496
No exit to room 2497
No exit to room 2498
No exit to room 2499
No exit to room 3406
No exit to room 3407
No exit to room 3569
No exit to room 3580
No exit to room 3585
No exit to room 3589
No exit to room 6030
No exit to room 6040
No exit to room 6156
No exit to room 6619
No exit to room 6628
No exit to room 6640
No exit to room 6641
No exit to room 6642
No exit to room 6643
No exit to room 6644
No exit to room 6645
No exit to room 6646
No exit to room 6647
No exit to room 6648
No exit to room 6649
No exit to room 6650
No exit to room 6651
No exit to room 7071
No exit to room 7130
No exit to room 7235
No exit to room 7268
No exit to room 7301
No exit to room 7903
No exit to room 8988
No exit to room 8989
No exit to room 8990
No exit to room 8991
No exit to room 8992
No exit to room 8993
No exit to room 8994
No exit to room 8995
No exit to room 8996
No exit to room 8997
No exit to room 8998
No exit to room 8999
No exit to room 10318
No exit to room 10320
No exit to room 10321
No exit to room 10322
No exit to room 10323
No exit to room 10324
No exit to room 10391
No exit to room 10398
No exit to room 10399
No exit to room 10400
No exit to room 10429
No exit to room 10498
No exit to room 10499
No exit to room 21067
No exit to room 21322
No exit to room 21325
No exit to room 21340
No exit to room 21390
No exit to room 21436
No exit to room 21499
No exit to room 24866
No exit to room 24873
No exit to room 24874
No exit to room 24875
No exit to room 24880
No exit to room 24884
No exit to room 24885
No exit to room 24886
No doubt some of those are deliberate, and some rooms are reached by mob programs, which this does not attempt to analyze. However it could be useful for checking if various areas have "useless" rooms.
The next thing to check is analyze our mobs. A preliminary investigation finds the number of mobs of each level, and what class they are:
-------------------- MOBS LIST --------------------------------
mob_levels = {}
classes = {}
-- look for mobs
for mob_vnum, mob_data in pairs (mobs) do
local alignment, level, mobthac0, ac, gold, exp =
string.match (mob_data.Stats1,
"^([%d-]+)%s+([%d-]+)%s+([%d-]+)%s+([%d-]+)%s+([%d-]+)%s+([%d-]+)$")
if level then
level = tonumber (level)
mob_levels [level] = mob_levels [level] or {} -- make this level if required
table.insert (mob_levels [level], mob_vnum)
else
print ("Mob vnum " .. mob_vnum .. " does not have a level")
end -- if
classes [mob_data.Class] = (classes [mob_data.Class] or 0) + 1
end -- for each room
for level, v in pairsByKeys (mob_levels) do
print ("Level " .. level .. " - " .. #v .. " mobs.")
end -- for each level
for k, v in pairsByKeys (classes) do
print ("Mob class: " .. k .. " - " .. v .. " items.")
end -- for each class
My results were:
Level 1 - 101 mobs.
Level 2 - 6 mobs.
Level 3 - 18 mobs.
Level 4 - 15 mobs.
Level 5 - 32 mobs.
Level 6 - 32 mobs.
Level 7 - 21 mobs.
Level 8 - 18 mobs.
Level 9 - 4 mobs.
Level 10 - 47 mobs.
Level 11 - 7 mobs.
Level 12 - 22 mobs.
Level 13 - 10 mobs.
Level 14 - 7 mobs.
Level 15 - 25 mobs.
Level 16 - 5 mobs.
Level 17 - 1 mobs.
Level 18 - 4 mobs.
Level 20 - 16 mobs.
Level 21 - 2 mobs.
Level 22 - 2 mobs.
Level 23 - 2 mobs.
Level 24 - 1 mobs.
Level 25 - 9 mobs.
Level 28 - 1 mobs.
Level 29 - 2 mobs.
Level 30 - 8 mobs.
Level 33 - 1 mobs.
Level 35 - 11 mobs.
Level 37 - 2 mobs.
Level 38 - 2 mobs.
Level 39 - 1 mobs.
Level 40 - 4 mobs.
Level 44 - 1 mobs.
Level 45 - 5 mobs.
Level 50 - 66 mobs.
Level 51 - 2 mobs.
Level 56 - 1 mobs.
Mob class: augurer - 2 items.
Mob class: baker - 1 items.
Mob class: blacksmith - 3 items.
Mob class: butcher - 1 items.
Mob class: cleric - 8 items.
Mob class: druid - 4 items.
Mob class: mage - 87 items.
Mob class: ranger - 3 items.
Mob class: thief - 7 items.
Mob class: vampire - 6 items.
Mob class: warrior - 392 items.
You could use code like this to see if your MUD is "balanced" - for example, above I see only 10 mobs in the level range 40 to 49.
At this stage I haven't done the code to write the area file back out, although I don't think it would be too hard. You could use that to make some sort of "global" change (like giving each mob some extra ability).
The extra file pairsbykeys.lua (used by the require statement) is the same as distributed with MUSHclient. The file tprint.lua is similar to the one that came with MUSHclient, but with Note changed to print, and Tell changed to io.write, as follows:
function tprint (t, indent, done)
-- show strings differently to distinguish them from numbers
local function show (val)
if type (val) == "string" then
return '"' .. val .. '"'
else
return tostring (val)
end -- if
end -- show
-- entry point here
done = done or {}
indent = indent or 0
for key, value in pairs (t) do
io.write (string.rep (" ", indent)) -- indent it
if type (value) == "table" and not done [value] then
done [value] = true
print (show (key), ":");
tprint (value, indent + 2, done)
else
io.write (show (key), "=")
print (show (value))
end
end
end
return tprint
|
- Nick Gammon
www.gammon.com.au, www.mushclient.com | Top |
|