Summary
Routines.Yield.Items.CraftItem (Py4GWCoreLib/routines_src/yield_src/items.py:499) always returns True after dispatching the craft, regardless of whether the crafted item actually reached inventory. A server-side rejection of the craft (wrong quantity, wrong price, anything) is silently reported as success, and every caller — both bundled levelers included — proceeds as if it were holding the item.
This is not hypothetical: it's exactly what cost real debugging time on our end tonight. A recipe-quantity bug in Factions Character Leveler.py (requesting 10 Wood Planks against a 5-plank recipe) was invisible for hours because CraftItem never reported the failure — the leveler's own if not result: check never fired, and the only visible symptom was the next step (EquipItem) failing with a generic "Failed to equip" message that pointed nowhere near the real cause.
Root cause (source-traced)
# items.py:488-496 — the function's OWN precondition check already polls correctly:
def _wait_for_crafter_item(output_model_id: int, timeout_ms: int = 2500, step_ms: int = 50):
wait_elapsed_ms = 0
while wait_elapsed_ms < timeout_ms:
for offered_item_id in GLOBAL_CACHE.Trading.Merchant.GetOfferedItems():
if int(GLOBAL_CACHE.Item.GetModelID(offered_item_id)) == int(output_model_id):
return offered_item_id
wait_elapsed_ms += step_ms
yield from wait(step_ms)
return 0
# items.py:499-531 — CraftItem itself:
GLOBAL_CACHE.Trading.Crafter.CraftItem(target_item_id, cost, trade_item_ids, quantity_list)
yield from wait(500)
return True
Every guard earlier in the function (material presence via GetFirstModelID, the crafter actually offering the item via _wait_for_crafter_item) is a precondition. The tail after the real dispatch call is the only place a postcondition could live, and it doesn't check anything — it just waits 500ms and reports success unconditionally.
Proposed fix
The function already has a working, polling precondition check one screen up (_wait_for_crafter_item, quoted above). The fix is the same pattern applied as a postcondition instead — poll for the output landing in inventory rather than trusting the dispatch call:
@staticmethod
def _wait_for_item_in_inventory(model_id: int, timeout_ms: int = 2500, step_ms: int = 50):
wait_elapsed_ms = 0
while wait_elapsed_ms < timeout_ms:
item_id = GLOBAL_CACHE.Inventory.GetFirstModelID(model_id)
if item_id:
return item_id
wait_elapsed_ms += step_ms
yield from wait(step_ms)
return 0
# CraftItem, replacing the unconditional wait(500)/return True:
GLOBAL_CACHE.Trading.Crafter.CraftItem(target_item_id, cost, trade_item_ids, quantity_list)
crafted_item_id = yield from Items._wait_for_item_in_inventory(
output_model_id,
timeout_ms=inventory_timeout_ms,
step_ms=inventory_step_ms,
)
return crafted_item_id != 0
Checks by output_model_id rather than target_item_id deliberately — a freshly crafted item gets its own new inventory item instance, so target_item_id (the crafter's offered-slot id, resolved before the craft) will never match it. inventory_timeout_ms/inventory_step_ms are already parameters on the function, just unused for this purpose currently.
Tested this exact fix locally against the real craft-rejection case (before your own crafter fix landed) — confirmed it correctly returns False and surfaces the real error one step earlier, instead of the misleading downstream "Failed to equip" message.
Scope
Affects every caller, not just the two bundled levelers — CraftArmor and any other Routines.Yield.Items.CraftItem call site inherits the same silent-success behavior.
Environment
- Guild Wars Reforged
Py4GW_Reforged, current main
- Found while diagnosing a craft failure in
Factions Character Leveler.py and Nightfall_leveler.py (unrelated recipe-quantity bug, already resolved on your end via 8b06492/da1cd74a) — this is the library-level defect that hid it, independent of that fix.
Summary
Routines.Yield.Items.CraftItem(Py4GWCoreLib/routines_src/yield_src/items.py:499) always returnsTrueafter dispatching the craft, regardless of whether the crafted item actually reached inventory. A server-side rejection of the craft (wrong quantity, wrong price, anything) is silently reported as success, and every caller — both bundled levelers included — proceeds as if it were holding the item.This is not hypothetical: it's exactly what cost real debugging time on our end tonight. A recipe-quantity bug in
Factions Character Leveler.py(requesting 10 Wood Planks against a 5-plank recipe) was invisible for hours becauseCraftItemnever reported the failure — the leveler's ownif not result:check never fired, and the only visible symptom was the next step (EquipItem) failing with a generic "Failed to equip" message that pointed nowhere near the real cause.Root cause (source-traced)
Every guard earlier in the function (material presence via
GetFirstModelID, the crafter actually offering the item via_wait_for_crafter_item) is a precondition. The tail after the real dispatch call is the only place a postcondition could live, and it doesn't check anything — it just waits 500ms and reports success unconditionally.Proposed fix
The function already has a working, polling precondition check one screen up (
_wait_for_crafter_item, quoted above). The fix is the same pattern applied as a postcondition instead — poll for the output landing in inventory rather than trusting the dispatch call:Checks by
output_model_idrather thantarget_item_iddeliberately — a freshly crafted item gets its own new inventory item instance, sotarget_item_id(the crafter's offered-slot id, resolved before the craft) will never match it.inventory_timeout_ms/inventory_step_msare already parameters on the function, just unused for this purpose currently.Tested this exact fix locally against the real craft-rejection case (before your own crafter fix landed) — confirmed it correctly returns
Falseand surfaces the real error one step earlier, instead of the misleading downstream "Failed to equip" message.Scope
Affects every caller, not just the two bundled levelers —
CraftArmorand any otherRoutines.Yield.Items.CraftItemcall site inherits the same silent-success behavior.Environment
Py4GW_Reforged, currentmainFactions Character Leveler.pyandNightfall_leveler.py(unrelated recipe-quantity bug, already resolved on your end via8b06492/da1cd74a) — this is the library-level defect that hid it, independent of that fix.