From 11bed1f55bcedb2ac4cd1d0362661b99b828f495 Mon Sep 17 00:00:00 2001 From: Pasi Kallinen Date: Mon, 26 Jan 2026 17:49:45 +0200 Subject: [PATCH] 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. --- doc/lua.adoc | 23 +++++++++++++++++++---- include/extern.h | 2 ++ src/nhlobj.c | 19 ++++++++++++++++++- src/nhlua.c | 29 +++++++++++++++++++++++++++++ src/sp_lev.c | 25 ++++++++++++++----------- test/test_des.lua | 21 +++++++++++++++++++++ test/test_obj.lua | 19 +++++++++++++++++++ 7 files changed, 122 insertions(+), 16 deletions(-) diff --git a/doc/lua.adoc b/doc/lua.adoc index 6d028085e..8ca4fd9c1 100644 --- a/doc/lua.adoc +++ b/doc/lua.adoc @@ -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 <> class. +Create an object and place it somewhere on the map. Returns the object as an <> 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 |=== diff --git a/include/extern.h b/include/extern.h index 40de54078..3d8261029 100644 --- a/include/extern.h +++ b/include/extern.h @@ -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 ### */ diff --git a/src/nhlobj.c b/src/nhlobj.c index 0579ddf47..a0535074c 100644 --- a/src/nhlobj.c +++ b/src/nhlobj.c @@ -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*/ diff --git a/src/nhlua.c b/src/nhlua.c index c83e81865..092c42e60 100644 --- a/src/nhlua.c +++ b/src/nhlua.c @@ -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 diff --git a/src/sp_lev.c b/src/sp_lev.c index 21357eada..25411aa1f 100644 --- a/src/sp_lev.c +++ b/src/sp_lev.c @@ -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); diff --git a/test/test_des.lua b/test/test_des.lua index b75f04c1c..151433d58 100644 --- a/test/test_des.lua +++ b/test/test_des.lua @@ -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 diff --git a/test/test_obj.lua b/test/test_obj.lua index d56a5ba55..68ef9147b 100644 --- a/test/test_obj.lua +++ b/test/test_obj.lua @@ -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