Merchant diagnostics
WPSFWEconomyDiagnostics() is a read-only native function available to consumer Lua contexts when supplied by the installed Framework. Check that it exists before calling it. Call on the game thread, such as inside a managed key callback. It takes no arguments and returns a plain scalar table, or nil and a reason. Wrong-thread calls are rejected before status or counter reads.
The function copies published content readiness and cumulative query, preview, and purchase counters. Preview and purchase timing probes are off during normal play. Set the WPS_ECONOMY_TRACE environment variable before launching to collect those timing families; remove it for normal play. Set WPSFWMerchantTrace=true in the Framework Lua environment before Economy preparation only when source-event logging is needed. Neither option adds a recurring row scan. It does not prepare or activate Economy, install hooks, look up game objects, change prices or reset counters. Internal Economy mutation functions remain private to the Framework. No background timer is needed.
-- Copyright (c) 2026 Wasted Potential Studios. All rights reserved.
Framework = require('../../WPS_Framework/Scripts/api')
local app = Framework.new {id='MyDiagnostics', version='0.1.0'}
app:retry {
max_attempts=120, interval_ms=500,
probe=function()
if not app:ready() then return end
assert(app:keybind {
key=121, modifiers={17,16},
callback=function()
if type(WPSFWEconomyDiagnostics)~='function' then
app:log('info','Merchant diagnostics are unavailable in this Framework package.')
return
end
local result,reason=WPSFWEconomyDiagnostics()
if not result then app:log('info',reason);return end
app:log('info',result.reason..' Native queries: '..result.queryCalls)
if result.previewTimingVersion==1 then
app:log('info',string.format(
'Preview selections: %d; cold quotes: %d; longest preview: %.3f ms.',
result.selectPreviewCalls,result.coldQuoteCalls,result.selectPreviewMaxUs/1000))
if result.previewClockHz==0 or result.selectPreviewClockFailures>0 or result.coldQuoteClockFailures>0 then
app:log('info','Some preview timing samples are unavailable; timing totals exclude those samples.')
end
end
if result.purchaseTimingVersion==1 then
app:log('info',string.format(
'Completed purchase scopes: %d; longest scope: %.3f ms.',
result.purchaseScopeCalls,result.purchaseScopeMaxUs/1000))
end
end
})
return true
end
}
The example uses Ctrl+Shift+F10 and ends its startup retry after registration or timeout. Choose a different available chord if another diagnostic tool owns it.
loaded describes the confirmed saved-content mount. stockReady, salesReady and featureReady describe separately published merchant readiness; mounting alone does not establish those features. reason, featureReason, phase and diagnostic explain the current state. mask and sales identify the immutable requested selection. baseline is retained profile metadata and is not a read of the game's global demand setting. Compare exposure as text to retain its full identity.
queryCalls, queryCacheBuilds, queryCachedCalls, queryRebuildingCalls, queryFailedCalls and queryClockFailures are cumulative for the loaded native component. queryLastUs and queryMaxUs describe measured query durations in microseconds. Cached and rebuilding paths have separate maxima. queryCachedOver1ms and queryCachedOver2ms count successful cached queries strictly above the named thresholds. A diagnostic call does not increment these counters.
Preview timing
Check previewTimingVersion==1 before reading the preview fields. Earlier diagnostic implementations can return the original fields without this family. previewClockHz is the native QueryPerformanceCounter frequency in ticks per second; zero means the clock is unavailable or has not yet been initialized. All exported fields ending in Us are already converted to microseconds. Divide by 1,000 for milliseconds; do not divide them by previewClockHz again.
| Prefix | Measured work |
|---|---|
selectPreview |
The admitted native preview-selection operation, including validation, cache lookup, any cold quote, and preview processing. |
coldQuote |
The cold native quote work entered when a preview needs a fresh quote. This is nested inside preview selection. |
Each prefix has Calls, FailedCalls, ClockFailures, TimedCalls, TotalUs, and MaxUs fields. Calls includes successful and rejected completed attempts. FailedCalls counts attempts that did not complete successfully. TimedCalls counts completed attempts with valid clock samples, including timed failures. ClockFailures counts attempts whose duration could not be measured. TotalUs and MaxUs include valid samples only; zero duration with clock failures does not establish zero cost. In-flight work is not included until its scope exits.
The two timing families overlap: cold quote time is part of preview-selection time. Do not add their totals together. These counters do not report Lua preparation duration or the complete widget Apply duration, and they are not a direct count of Lua cache hits or visible rows.
Purchase, refresh, and cleanup timing
Check purchaseTimingVersion==1 before reading this family. It uses the same previewClockHz clock and the same microsecond units. Each prefix below has Calls, FailedCalls, ClockFailures, TimedCalls, TotalUs, and MaxUs fields.
| Prefix | Measured work |
|---|---|
purchaseScope |
The admitted Economy begin callback through end cleanup, including the native purchase RPC between those callbacks; excludes earlier guard and host checks. |
purchaseCleanup |
The corresponding Economy end-cleanup work inside the purchase scope. |
grantOriginal |
The original native grant call. |
refreshLightTotal / refreshLightOriginal |
The complete intercepted light refresh and the original native call within it. |
refreshRebuildTotal / refreshRebuildOriginal |
The complete intercepted rebuilding refresh and the original native call within it. |
completeBuyTotal / completeBuyOriginal |
The complete intercepted Buy-response handler and the original native call within it. |
previewRestore |
Preview restoration work. |
These measurements overlap: an original-call duration belongs inside its total, and grant or cleanup work can belong inside a purchase scope. Do not sum these families as independent elapsed time. A snapshot taken inside a purchase-triggered UI refresh cannot include that still-running outer purchase scope; read again after the scope completes on a later explicit event.
purchaseScopeFailedCalls includes declined admission or abnormal scope exit. In the other purchase families, failure denotes abnormal function exit; a normal native rejection may still be a normally completed measured call. Neither zero failures nor a normal return proves that the player received an item or that a transaction succeeded.
purchaseScopeUnmeasured counts game-thread begin attempts that could not be pushed because the existing eight-slot scope stack was full or already overflowing. It is separate from clock failures. previewRestoreScanned counts preview-map records visited, including records belonging to a different owner. previewRestoreRestored counts records where the existing restoration path confirmed an owned field already had its original value or successfully wrote the original value back; it does not count individual fields. Scanned records remain counted after abnormal exit, while restored records are added only when restoration returns normally.
Within one loaded native component, preview and purchase counts, totals, and maxima are nondecreasing. Counts and accumulated native ticks saturate at the signed 64-bit maximum. Reading, reopening a shop, changing worlds, or invalidating a quote does not reset them. There is no diagnostic reset operation. Off-thread work does not contribute to these timing counters. queryLastUs, readiness flags, and explanatory text are observations rather than monotonic counters.
Take explicit before/after snapshots to compare a test interval. Discard a comparison if the game or native component restarted, or if the preview clock frequency changed. A mean duration for that interval uses the increase in TotalUs divided by the increase in TimedCalls, when that count is positive. Cumulative maxima cannot be subtracted to obtain an interval maximum. These fields do not provide a full latency distribution, game frame times or proof of working transactions. Keep gameplay tests and matched performance captures separate; a diagnostic snapshot is not gameplay or performance acceptance.
The complete additive result contract is in economy-diagnostics.schema.json. An unavailable diagnostic endpoint does not require disabling unrelated mod features.
