Summary
The MHF client (mhf.exe) crashes when entering the forge if a shop_type=10 (item shop) tab returns too many rows in MSG_MHF_ENUMERATE_SHOP (0x00A5). The server itself does not crash — the TCP connection is forcibly closed by the client and Erupe just logs the disconnect.
Reproduction
- Seed
shop_items so that one (shop_type=10, shop_id=N) slot has ~420 rows (the "special item" tab in our case).
- Walk a character into the forge.
- Client sends 9 successive
MSG_MHF_ENUMERATE_SHOP requests (ShopID=0..8); the large response for ShopID=8 is delivered, then ~2s later mhf.exe crashes.
A separate test server with that tab trimmed loads the forge normally, which confirms row count (or one bad row) is the trigger.
Server log excerpt
... opcode_name=MSG_MHF_ENUMERATE_SHOP data_bytes=21 ... (ShopID=8)
... opcode_name=MSG_SYS_ACK data_bytes=15374 ...
WARN Connection error, exiting recv loop error="wsarecv: An existing connection was forcibly closed by the remote host."
INFO Player logout initiated
Suspected cause
The original client almost certainly has a fixed-size internal buffer per item-shop tab. The request advertises Limit=0x0200 (512), but that's an upper bound the client claims to support — the renderer cannot actually handle that many entries. We know 420 rows crashes and a trimmed tab loads; the real ceiling is somewhere in between and not yet bisected.
Relevant code: server/channelserver/handlers_shop.go case 10 (≈ line 225). The handler honors pkt.Limit but applies no server-side sanity cap.
Workarounds (today)
- Curate the seed. Split oversized item-shop tabs across multiple
shop_ids, or remove duplicates. This is the proper fix — no data is lost.
- Manually trim the offending
(shop_type, shop_id) rows in shop_items until the forge loads.
Possible server-side fix
Add a conservative cap in handlers_shop.go for ShopType=10:
const maxItemShopRows = 256 // tentative; needs bisect
if len(items) > maxItemShopRows {
items = items[:maxItemShopRows]
}
Caveats:
- 256 is a guess; the true ceiling needs to be bisected (try 384, 320, …) before being committed as a constant.
- Truncation is lossy — players silently lose access to the trimmed rows. A cap is only a safety net so a bad seed cannot soft-brick the forge; it is not a replacement for fixing the data.
- Scope only
ShopType=10. Gacha (case 2) and exchange shops (cases 3–9) have different size profiles and are not implicated.
Asks
- If you have hit the forge softlock, please report your
shop_items row counts per (shop_type=10, shop_id) so we can narrow the threshold.
- Help bisecting the exact safe row count would be appreciated.
Summary
The MHF client (
mhf.exe) crashes when entering the forge if ashop_type=10(item shop) tab returns too many rows inMSG_MHF_ENUMERATE_SHOP(0x00A5). The server itself does not crash — the TCP connection is forcibly closed by the client and Erupe just logs the disconnect.Reproduction
shop_itemsso that one(shop_type=10, shop_id=N)slot has ~420 rows (the "special item" tab in our case).MSG_MHF_ENUMERATE_SHOPrequests (ShopID=0..8); the large response forShopID=8is delivered, then ~2s latermhf.execrashes.A separate test server with that tab trimmed loads the forge normally, which confirms row count (or one bad row) is the trigger.
Server log excerpt
Suspected cause
The original client almost certainly has a fixed-size internal buffer per item-shop tab. The request advertises
Limit=0x0200(512), but that's an upper bound the client claims to support — the renderer cannot actually handle that many entries. We know 420 rows crashes and a trimmed tab loads; the real ceiling is somewhere in between and not yet bisected.Relevant code:
server/channelserver/handlers_shop.gocase 10(≈ line 225). The handler honorspkt.Limitbut applies no server-side sanity cap.Workarounds (today)
shop_ids, or remove duplicates. This is the proper fix — no data is lost.(shop_type, shop_id)rows inshop_itemsuntil the forge loads.Possible server-side fix
Add a conservative cap in
handlers_shop.goforShopType=10:Caveats:
ShopType=10. Gacha (case 2) and exchange shops (cases 3–9) have different size profiles and are not implicated.Asks
shop_itemsrow counts per(shop_type=10, shop_id)so we can narrow the threshold.