Lua tests: generation of each object

Test generation of every object, both via des.object and obj.new.
Expose FIRST_OBJECT and LAST_OBJECT numbers to lua.
Add lua nh.int_to_objname, a function to convert integer value to
object base name and class.
Allow creating new nethack lua object by specifying id and class.
This commit is contained in:
Pasi Kallinen
2026-01-26 17:49:45 +02:00
parent b5ca1a3ed8
commit 11bed1f55b
7 changed files with 122 additions and 16 deletions

View File

@@ -259,6 +259,17 @@ Example:
local str = nh.ing_suffix("foo");
=== int_to_objname
Convert integer value to object name and class.
Returns two strings, the object base name and the class character.
The returned strings may be empty if an error occurred.
Example:
local oname, oclass = nh.int_to_objname(45);
=== int_to_pmname
Convert integer value to monster type name.
@@ -850,7 +861,7 @@ Example:
=== object
Create an object. Returns the object as an <<Obj>> class.
Create an object and place it somewhere on the map. Returns the object as an <<Obj>> class.
The table parameter accepts the following:
[options="header"]
@@ -874,8 +885,8 @@ The table parameter accepts the following:
| greased | boolean | Is the object greased?
| broken | boolean | Is the object broken?
| achievement | boolean | Is there an achievement attached to the object?
| x, y | int | Coordinates on the level
| coord | table | x,y coordinates in table format
| x, y | int | Coordinates on the level; defaults to a random location.
| coord | table | x,y coordinates in table format; defaults to a random location.
| montype | string | Monster id or class
| historic | boolean | Is statue historic?
| male | boolean | Is statue male?
@@ -1380,11 +1391,13 @@ Handling objects via obj-class.
=== new
Create a new object via wishing routine.
Create a new object, either via wishing routine, or specifying object name and class.
Unlike des.object, does not place the object anywhere.
Example:
local o = obj.new("rock");
local o = obj.new({ id = "invisibility", class = "!" });
=== isnull
@@ -1576,6 +1589,8 @@ These constants are in the `nhc` table.
| NUMMONS | Number of different monster types
| LOW_PM | First monster type id. See <<_int_to_pmname>>.
| HIGH_PM | Last monster type id. See <<_int_to_pmname>>.
| FIRST_OBJECT | First object type id. See <<_int_to_objname>>.
| LAST_OBJECT | Number of object type ids. See <<_int_to_objname>>.
| DLB | 1 or 0, depending if NetHack is compiled with DLB
|===

View File

@@ -3033,6 +3033,8 @@ extern int nhl_abs_coord(lua_State *) NONNULLARG1;
extern void update_croom(void);
extern const char *get_trapname_bytype(int);
extern void l_register_des(lua_State *) NONNULLARG1;
extern int get_table_objclass(lua_State *) NONNULLARG1;
extern int get_table_objtype(lua_State *) NONNULLARG1;
#endif /* !CROSSCOMPILE || CROSSCOMPILE_TARGET */
/* ### spell.c ### */

View File

@@ -345,12 +345,13 @@ DISABLE_WARNING_UNREACHABLE_CODE
/* create a new object via wishing routine */
/* local o = obj.new("rock"); */
/* local o = obj.new({ id = "food ration", class = "%" }); */
staticfn int
l_obj_new_readobjnam(lua_State *L)
{
int argc = lua_gettop(L);
if (argc == 1) {
if (argc == 1 && lua_type(L, 1) == LUA_TSTRING) {
char buf[BUFSZ];
struct obj *otmp;
@@ -360,6 +361,22 @@ l_obj_new_readobjnam(lua_State *L)
otmp = NULL;
(void) l_obj_push(L, otmp);
return 1;
} else if (argc == 1 && lua_type(L, 1) == LUA_TTABLE) {
short id = get_table_objtype(L);
xint16 class = get_table_objclass(L);
struct obj *otmp;
if (id >= FIRST_OBJECT) {
otmp = mksobj(id, TRUE, FALSE);
} else {
class = def_char_to_objclass(class);
if (class >= MAXOCLASSES)
class = RANDOM_CLASS;
otmp = mkobj(class, FALSE);
}
lua_pop(L, 1);
(void) l_obj_push(L, otmp);
return 1;
} else
nhl_error(L, "l_obj_new_readobjname: Wrong args");
/*NOTREACHED*/

View File

@@ -32,6 +32,7 @@ staticfn int nhl_dump_fmtstr(lua_State *);
#endif /* DUMPLOG */
staticfn int nhl_dnum_name(lua_State *);
staticfn int nhl_int_to_pm_name(lua_State *);
staticfn int nhl_int_to_obj_name(lua_State *);
staticfn int nhl_stairways(lua_State *);
staticfn int nhl_pushkey(lua_State *);
staticfn int nhl_doturn(lua_State *);
@@ -1185,6 +1186,31 @@ nhl_int_to_pm_name(lua_State *L)
return 1;
}
/* convert integer to object type name and class */
/* local oname,oclass = int_to_objname(25); */
staticfn int
nhl_int_to_obj_name(lua_State *L)
{
int argc = lua_gettop(L);
if (argc == 1) {
char buf[8];
lua_Integer i = luaL_checkinteger(L, 1);
if (i >= 0 && i < NUM_OBJECTS && OBJ_NAME(objects[i])) {
lua_pushstring(L, OBJ_NAME(objects[i]));
buf[0] = def_oc_syms[(int)objects[i].oc_class].sym;
buf[1] = '\0';
lua_pushstring(L, buf);
} else {
lua_pushstring(L, "");
lua_pushstring(L, "");
}
} else
nhl_error(L, "Expected an integer parameter");
return 2;
}
DISABLE_WARNING_UNREACHABLE_CODE
/* because nhl_error() does not return */
@@ -1858,6 +1884,7 @@ static const struct luaL_Reg nhl_functions[] = {
#endif /* DUMPLOG */
{ "dnum_name", nhl_dnum_name },
{ "int_to_pmname", nhl_int_to_pm_name },
{ "int_to_objname", nhl_int_to_obj_name },
{ "variable", nhl_variable },
{ "stairways", nhl_stairways },
{ "pushkey", nhl_pushkey },
@@ -1876,6 +1903,8 @@ static const struct {
{ "NUMMONS", NUMMONS },
{ "LOW_PM", LOW_PM },
{ "HIGH_PM", HIGH_PM },
{ "FIRST_OBJECT", FIRST_OBJECT },
{ "LAST_OBJECT", NUM_OBJECTS-1 },
#ifdef DLB
{ "DLB", 1 },
#else

View File

@@ -117,9 +117,7 @@ staticfn int find_montype(lua_State *, const char *, int *);
staticfn int get_table_montype(lua_State *, int *);
staticfn lua_Integer get_table_int_or_random(lua_State *, const char *, int);
staticfn int get_table_buc(lua_State *);
staticfn int get_table_objclass(lua_State *);
staticfn int find_objtype(lua_State *, const char *);
staticfn int get_table_objtype(lua_State *);
staticfn int find_objtype(lua_State *, const char *, char);
staticfn const char *get_mkroom_name(int) NONNULL;
staticfn int get_table_roomtype_opt(lua_State *, const char *, int);
staticfn int get_table_traptype_opt(lua_State *, const char *, int);
@@ -3443,7 +3441,7 @@ get_table_buc(lua_State *L)
return curse_state;
}
staticfn int
int
get_table_objclass(lua_State *L)
{
char *s = get_table_str_opt(L, "class", NULL);
@@ -3455,13 +3453,14 @@ get_table_objclass(lua_State *L)
return ret;
}
/* find object otyp by text s (optionally considering oclass) */
staticfn int
find_objtype(lua_State *L, const char *s)
find_objtype(lua_State *L, const char *s, char oclass)
{
if (s && *s) {
int i;
const char *objname;
char class = 0;
char class = def_char_to_objclass(oclass);
/* In objects.h, some item classes are defined without prefixes
(such as "scroll of ") in their names, making some names (such
@@ -3479,6 +3478,9 @@ find_objtype(lua_State *L, const char *s)
{ NULL, 0 }
};
if (class == MAXOCLASSES)
class = 0;
if (strstri(s, " of ")) {
for (i = 0; class_prefixes[i].prefix; i++) {
const char *p = class_prefixes[i].prefix;
@@ -3523,11 +3525,12 @@ find_objtype(lua_State *L, const char *s)
return STRANGE_OBJECT;
}
staticfn int
int
get_table_objtype(lua_State *L)
{
char *s = get_table_str_opt(L, "id", NULL);
int ret = find_objtype(L, s);
char oclass = get_table_objclass(L);
int ret = find_objtype(L, s, oclass);
Free(s);
return ret;
@@ -3586,7 +3589,7 @@ lspo_object(lua_State *L)
tmpobj.id = STRANGE_OBJECT;
} else {
tmpobj.class = -1;
tmpobj.id = find_objtype(L, paramstr);
tmpobj.id = find_objtype(L, paramstr, -1);
}
} else if (argc == 2 && lua_type(L, 1) == LUA_TSTRING
&& lua_type(L, 2) == LUA_TTABLE) {
@@ -3599,7 +3602,7 @@ lspo_object(lua_State *L)
tmpobj.id = STRANGE_OBJECT;
} else {
tmpobj.class = -1;
tmpobj.id = find_objtype(L, paramstr);
tmpobj.id = find_objtype(L, paramstr, -1);
}
} else if (argc == 3 && lua_type(L, 2) == LUA_TNUMBER
&& lua_type(L, 3) == LUA_TNUMBER) {
@@ -3613,7 +3616,7 @@ lspo_object(lua_State *L)
tmpobj.id = STRANGE_OBJECT;
} else {
tmpobj.class = -1;
tmpobj.id = find_objtype(L, paramstr);
tmpobj.id = find_objtype(L, paramstr, -1);
}
} else {
lcheck_param_table(L);

View File

@@ -180,6 +180,27 @@ function test_object()
des.object({ name = "Random object" });
des.object({ class = "*", name = "Random stone" });
des.object({ id ="broadsword", name = "Dragonbane" })
for i = nhc.FIRST_OBJECT, nhc.LAST_OBJECT do
local oid, oclass = nh.int_to_objname(i);
if (oid ~= "") then
local o = des.object({ id = oid, class = oclass });
local o_t = o:totable();
-- crysknife reverts to worm tooth on the floor
if not(oid == "crysknife" and o_t.otyp_name == "worm tooth") then
if (o_t.otyp_name ~= oid) then
error("object name \"" .. o_t.otyp_name .. "\" created, wanted \"" .. oid .. "\"");
end
if (o_t.oclass ~= oclass) then
local str = string.format("object class \"%s\" created, wanted \"%s\" (%s)", o_t.oclass, oclass, oid);
error(str);
end
end
end
end
des.reset_level();
des.level_init();
end

View File

@@ -82,3 +82,22 @@ box:addcontent(o5);
local o6 = obj.new("statue");
o6:addcontent(obj.new("spellbook"));
-- generate one of each object, check the name and class matches
for i = nhc.FIRST_OBJECT, nhc.LAST_OBJECT do
local oid, oclass = nh.int_to_objname(i);
if (oid ~= "") then
local oi = obj.new({ id = oid, class = oclass });
local oi_t = oi:totable();
if (oi_t.otyp_name ~= oid) then
error("object name \"" .. oi_t.otyp_name .. "\" created, wanted \"" .. oid .. "\"");
end
if (oi_t.oclass ~= oclass) then
local str = string.format("object class \"%s\" created, wanted \"%s\" (%s)", oi_t.oclass, oclass, oid);
error(str);
end
end
end