Skip to content

workorder: require item_subtype for item-making orders - #1650

Open
eltmon wants to merge 1 commit into
DFHack:masterfrom
eltmon:workorder-require-item-subtype
Open

eltmon wants to merge 1 commit into
DFHack:masterfrom
eltmon:workorder-require-item-subtype

Conversation

@eltmon

@eltmon eltmon commented Oct 15, 2026

Copy link
Copy Markdown

Summary

workorder creates a manager order with item_subtype = -1 when the JSON omits "item_subtype" for a job whose product has subtypes (shoes, armor, weapons, tools, ammo, trap components, toys, siege ammo). DF validates the order and spawns jobs from it, but segfaults in its job-name code whenever one of those jobs is named. The vanilla UI cannot create such an order, and once created it persists in the save, so the fort crashes again after every reload.

This PR makes create_orders() refuse such orders with an error that names a valid subtype, e.g.:

Manager order MakeShoes needs an item_subtype (e.g. ITEM_SHOES_SHOES)

Repro

DF 53.16 (Steam, Linux), DFHack 53.16-r2:

workorder "{\"job\":\"MakeShoes\",\"material_category\":[\"leather\"],\"amount_total\":4}"

The order is created and validated. As soon as a job from it is named, DF crashes. In our fort this happened 8 times before we traced it to the order.

Crash via DFHack naming the job:

dwarfort 0x1cab6d9 <- dwarfort 0x13657f8 <- libdfhack DFHack::Job::getName

Crash via DF drawing the job:

dwarfort 0x1cab6d9 <- dwarfort 0xbdc603 <- dwarfort 0x792311 <- ... interfacest::loop

Fix

The guard sits right after item_subtype is resolved and before the meal_ingredients / material handling. If the order's subtype is still -1, it works out the produced item type (the explicit item_type, otherwise df.job_type.attrs[job].item). If df.itemdef_<type>st exists and has entries, it raises a qerror naming the first defined subtype as an example. Item types without itemdefs (tables, bars, scepters, ...) are unaffected.

FOOD is exempt. PrepareMeal orders keep item_subtype = -1 in vanilla and store the ingredient count in mat_type. A vanilla meal order in a live fort reads item_type -1, item_subtype -1, mat_type 2, even though itemdefs.food has 3 entries. Without the exemption, workorder PrepareMeal 10 would be wrongly refused.

The shorthand form (workorder MakeShoes 4) also goes through create_orders(), so the guard covers it too.

Testing

Live, on DF 53.16 / DFHack 53.16-r2, with the guard applied to the installed workorder.lua:

  • MakeShoes without item_subtype: refused with Manager order MakeShoes needs an item_subtype (e.g. ITEM_SHOES_SHOES)
  • MakeScepter (no subtypes exist): queued as before
  • MakeShoes with "item_subtype":"ITEM_SHOES_SHOES": queued as before

I added the FOOD exemption after those runs. To check it without queuing more orders in the fort, I evaluated the final guard expression against the live raws with item_subtype -1:

Job Result
PrepareMeal ok
MakeScepter ok
ConstructTable ok
MakeCharcoal ok
EncrustWithGems ok
ShearCreature ok
SmeltOre ok
CustomReaction ok
MakeShoes refused (e.g. ITEM_SHOES_SHOES)
MakeTool refused (e.g. ITEM_TOOL_CAULDRON)
MakeWeapon refused (e.g. ITEM_WEAPON_WHIP)
AssembleSiegeAmmo refused (e.g. ITEM_SIEGEAMMO_BALLISTA)

All the subtyped-item orders in the bundled data/orders/*.json libraries already specify item_subtype. Those files are imported by orders, not by this script, so they are unaffected either way.

Changelog entry added under Future / Fixes.

🤖 Generated with Claude Code

An order for an item type that has subtypes (shoes, armor, weapons, tools,
ammo, ...) created without "item_subtype" was left at item_subtype -1. DF
accepts and validates such an order, but segfaults in its job-name code
whenever a resulting job is named (by dfhack.job.getName or by the game
drawing the job), and the bad order persists in the save.

Refuse these orders with an error that names a valid subtype. Meals
(PrepareMeal / FOOD) keep item_subtype -1 as in vanilla and are exempt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@SilasD SilasD left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like you to make one change and re-test.

basically I would like you to use our API calls instead of probing the table of object classes.

I'm not asking you to change all of the existing code to use the API calls, just in your added code.

(but if you want to, I won't say no. just remember, bigger changes need more testing.)


also, this is not too important, but the DFHack Launcher (and the console window) support an alternate way to enter commands which is very helpful when dealing with double-quotes.

If the first non-whitespace character is :, the command is parsed in an alternative mode. The non-whitespace characters following the : are the command name, and the remaining part of the line is used verbatim as the first argument.

workorder "{\"job\":\"MakeShoes\",\"material_category\":[\"leather\"],\"amount_total\":4}"
can also be written as
:workorder {"job":"MakeShoes","material_category":["leather"],"amount_total":4}

Comment thread workorder.lua
Comment on lines +237 to +243
if produced and produced ~= df.item_type.NONE and produced ~= df.item_type.FOOD then
local def = df['itemdef_' .. string.lower(df.item_type[produced]) .. 'st']
if def and #def.get_vector() > 0 then
qerror(("Manager order %s needs an item_subtype (e.g. %s)"):format(
df.job_type[order.job_type], def.get_vector()[0].id))
end
end

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fully understand that Claude copied working code, but the code it copied isn't really correct.

It should use our API calls

dfhack.items.getSubtypeCount(item_type)
Returns the number of raw-defined subtypes of the given item type, or -1 if not applicable.

dfhack.items.getSubtypeDef(item_type, subtype)
Returns the raw definition for the given item type and subtype, or nil if invalid.

for example (minimal testing):

Suggested change
if produced and produced ~= df.item_type.NONE and produced ~= df.item_type.FOOD then
local def = df['itemdef_' .. string.lower(df.item_type[produced]) .. 'st']
if def and #def.get_vector() > 0 then
qerror(("Manager order %s needs an item_subtype (e.g. %s)"):format(
df.job_type[order.job_type], def.get_vector()[0].id))
end
end
if produced and produced ~= df.item_type.NONE and produced ~= df.item_type.FOOD
and dfhack.items.getSubtypeCount(produced) > -1
then
local example_def = dfhack.items.getSubtypeDef(produced, 0)
if example_def then
qerror(("Manager order %s needs an item_subtype (e.g. %s)"):format(
df.job_type[order.job_type], example_def.id))
end
end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants