Skip to content

Routines.Yield.Items.CraftItem reports success on a craft that never happened #9

Description

@Royel-Payne

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.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions