Wasted Potential Studios · Dune: Awakening

Framework API reference release artwork

WPS Dune Framework API

Copyright (c) 2026 Wasted Potential Studios. All rights reserved.

WPS Dune Framework 1.7.1 provides shared services and a public creation API for Lua mods and Lua/pak hybrids. Keep the Framework installed once as Mods\WPS_Framework; each consumer declares it and WPS Dune Launcher as standalone requirements. Native UE4SS mods initialize before Lua and use the native SDK instead.

Check the installed Framework version before using a method or option with a version requirement. A pure Pak does not require the Framework solely for package import. A Lua companion using this API does require it, but API registration does not establish that the game's container loader accepts the accompanying Pak. The author guide separates folder/manifest validation, Dune container-format requirements and game acceptance.

F.RELEASE identifies the installed package release. F.VERSION remains the shared API compatibility version (1.5.0), so previously loaded 1.5.0 consumers continue to recognize Framework readiness. app:capabilities().framework_release exposes the package release alongside the existing framework compatibility value. Use the package release to check release-specific additions; retain the shared compatibility value for dependency handshakes. F.BUILD, profile_build and report profileBuild retain the reference-catalog build for existing consumers. Read compatibility.identity for the installation version and private_layout.profile for the currently checked Inventory layout; the reference-catalog fields do not gate those capabilities.

Compatibility checks distinguish reflected interfaces from private Inventory layouts. The native catalogue includes Inventory-layout profiles for Steam builds 25486029, 25611704, 25690505 and 25789117, and Microsoft PC packages 2.0.7.0, 2.0.8.0, 2.0.9.0, 2.0.10.0, 2.0.11.0 and 2.0.12.0, using UE4SS 3.0.1-1140-gf58e8f84. Reflected anchors are checked separately. Stack and water capabilities retain independent complete native checks; an Inventory-layout match does not enable them. General helper availability does not establish that an arbitrary game function or property is safe to use. Each gameplay branch requires separate loaded-game acceptance. Current Steam 25789117 and Xbox PC 2.0.11.0 native bodies were checked independently. The Xbox evidence is a captured loaded-code image, not an original installed-file fingerprint. Earlier Microsoft Inventory profiles remain available. Native inspection is separate from loaded-world gameplay acceptance.

Activation and cleanup

Loading the API, creating a context and checking dependency readiness do not start a gameplay service. Framework startup publishes readiness and installation metadata without checking the full executable or attaching inventory hooks. A requested native capability performs its own verification before use; proof is cached for the running process.

The service broker wakes when a client posts a request. It constructs only the requested provider. Active service ownership or cross-mod messaging work maintains its bounded clock; when the final request has been restored and acknowledged, the broker sleeps and the service client stops its heartbeat. Cleanup failures retain ownership and retry instead of reporting a completed release. Creating an unused context leaves no recurring broker timer.

An explicit compatibility inspection requests private-layout proof. Gameplay operations using app:gate() retain their per-operation signature and authoritative Singleplayer checks. Direct scheduling, input, object notifications and game-event observation activate only through their corresponding calls and retain their own cancellation contracts.

app:creation() is a separate startup-data capability. Immutable namespaced rows are registered before world loading, and their owned local menu presentation can activate without a player session. Committed definitions and unlock membership remain for the process lifetime. Actor creation through app:compose() and placeable props reconstruction still requires the authoritative local session gate. Local definition registration does not establish multiplayer compatibility or install content on a remote server. See the creation guide for ownership and lifetime details.

Inspect compatibility

Call app:compatibility() on the game thread for the current compatibility record. It does not change gameplay or resolve a player character.

Field Meaning
identity store (steam, microsoft or unknown), Steam build, Microsoft version and package, and a short path-free evidence description. An optional privateProfile records a verified Inventory layout. A folder without recognized metadata retains store='unknown'; the native profile does not invent its build or package identity. Inapplicable identity strings are empty.
private_layout supported, optional verified profile, and an optional reason when the private layout is unavailable.
reflected supported for the four core session signatures, optional reason, validation='per_operation' and a requirement description. This is not proof that a world is ready or every API is compatible.
scopes The available scope names: reflected, private, merchant, backpack and properties.
default_scope private, preserving the default requirement for existing callers.

app:gate(scope) also requires dependency readiness and a fresh local authoritative Singleplayer session. Omitting the scope keeps the private-layout gate. Use reflected for reflected interfaces; each game, engine or scalar-patch operation then performs its own checks. Private scopes retain their verified-layout requirement. An unknown scope returns unsupported_capability; unavailable private support returns unsupported_build with an explanation. Never treat a reflected gate result as permission to use an unverified memory offset.

The merchant scope validates the vendor table and row interfaces without requiring private inventory offsets. The legacy backpack scope requires a checked Inventory layout and validates its reflected inventory, training, settings and callback interfaces. The maintained backpack_capacity service uses the current native Inventory proof with its own reflected validator. Current native profile IDs include steam-25486029, steam-25611704, steam-25690505, steam-25789117, microsoft-2.0.7.0, microsoft-2.0.8.0, microsoft-2.0.9.0, microsoft-2.0.10.0, microsoft-2.0.11.0 and microsoft-2.0.12.0. These name inspected code layouts, not a list of allowed game versions. A matching native proof remains available when installation metadata changes; the dedicated native Inventory bridge can provide a newer checked profile without a separate Lua name list. The native bridge checks the current-volume getter, inventory-array body and tracker-capacity body. Inventory layout and capacity setter bodies may be matched independently only when their native profiles explicitly declare the same proven ABI group; all exact body checks remain required. It exposes a profile ID rather than game code, pointers or offsets. An unsupported native profile leaves that private capability unavailable.

The disk fallbacks pc-25486029 and pc-25611704 are each bounded to their corresponding executable identity, four 4096-byte native windows and inventory diagnostic. They refuse malformed, changed or unreadable files and do not infer a storefront. Each private property, native service and reflected operation still performs its own required interface and session checks.

The same compatibility record appears in app:capabilities() and app:game_capabilities(). A report made on the game thread also includes it. Installation metadata, matching declarations, world readiness and observed gameplay are separate evidence.

Start a consumer

Put this in your mod's Scripts/main.lua:

-- Copyright (c) 2026 Your Studio. All rights reserved.
Framework = require('../../WPS_Framework/Scripts/api')
local app = Framework.new { id = 'MyFeature', version = '1.0.0' }

app:retry {
    interval_ms = 2000,
    max_attempts = 30,
    probe = function()
        local session = app:session()
        if session.ok then return session end
    end,
    on_ready = function(session)
        app:log('info', 'Local character is ready.')
        app:status('active', 'Feature ready.')
        -- Resolve and verify your feature's game objects here.
    end,
    on_timeout = function()
        app:status('waiting', 'Waiting for a local Singleplayer character.')
    end
}

The relative import uses the pinned runtime's mod searcher. Each Lua mod has its own state. Framework callbacks, Lua tables and globals stay inside their owner; cooperative ownership uses scalar shared values on the game thread.

Shared game services

These services let an author describe a supported change while the Framework owns the game-specific implementation. A consumer enables only the services it requests. WPS Economy, Expanded Inventory and Stacks Up require the separate Framework download.

Service requests can be made during script loading. A valid request returns a local handle immediately, initially pending; it does not mean the game change has succeeded. Invalid options return nil, {code, message} before anything is queued. Read the handle's acknowledged status before reporting that your feature is active.

The generic stack service is available to every modder through app:stack_limits{preset=2} since Framework 1.4.5. Use a current Framework package meeting the generated starter's declared minimum and its exact UE4SS 1140 dependency. Framework 1.7.1 accepts integer factors from 1 to 1000, or 'maximum', for existing stackable items. An accepted handle:update{preset=2} queues a bounded definition and loaded-item revalidation even when the preset is unchanged. Temporary readiness or session loss retains unfinished work, while world/table identity checks protect recovery. Stop the service and await stopped before closing the context. Inventory signals belong to the checked shared implementation; consumers retain the same request and handle APIs.

Checked inventory-content signals refresh affected item IDs directly after identity and limit validation. Queue reset or overflow requests one bounded discovery pass. The Framework owns this native event capability and requires it for stack activation; there is no additional public inventory-event method to register or hook. External ownership or unproven write conflicts remain visible until their cause is resolved and the consumer requests a new revision.

local offers, problem = app:merchant_offers {
    target = 'choam',
    offers = { {
        id = 'SMG_Unique_LargeMag_06_Schematic',
        price = 70000,
        stock = 0,
        currency = 'Solaris'
    } }
}
assert(offers, problem and problem.message)

app:defer(5000, function()
    local result = offers:status()
    app:log('info', result.code .. ': ' .. result.message)
end)

merchant_offers adds item-specific schematic offers to the supported CHOAM vendor row. It does not grant items or expand the vendor's general query. Use python kit/wps.py items <search> to find an identifier in the build-specific catalog. Each handle accepts 1–64 distinct supported identifiers, integer price values from 2 to 2147483647, integer stock values from 0 to 2147483647, and currency='Solaris'. Stock zero is the game's existing unlimited-stock representation for this row. Test the displayed price, availability and an actual purchase for your chosen offer.

price retains its published multiplier contract: the service's native factor is applied to the current item base. Captured supported schematics had a base of one, which made the factor numerically equal to the baseline Solaris price in those captures. A changed base changes the resulting reference; this is not an absolute-price API. Use app:economy contributions for a canonical price policy on a verified item outside that schematic catalog.

Agreeing contributors may share the exact offer. With saved Economy, the shared native journal coordinates schematic and Economy leases with separate internal scopes. A matching pre-existing offer can be adopted read-only; adopting it never grants later overwrite authority, and its final withdrawal leaves the original in place. Created offers are restored or removed only while their checked record and ownership still agree. Outside changes preserve the record and leave cleanup pending. The legacy provider remains available without saved content. Public fields and units stay unchanged.

Several authors can contribute different offers. Identical contributions can coexist; different values for the same item produce a conflict. Native and unrelated offers remain in place. Stopping a contribution removes only entries still owned by the service and still matching its applied values; another contribution can keep a shared offer active. A conflicting replacement retains the previously accepted contribution and reports retained=true.

local capacity, problem = app:backpack_capacity { multiplier = 2 }
assert(capacity, problem and problem.message)

-- Later, replace this handle's complete request:
local accepted, updateProblem = capacity:update { multiplier = 5 }
-- accepted means queued; check capacity:status() for the game result.

-- When this feature is intentionally disabled:
capacity:stop()

backpack_capacity supports multipliers integers from 1 to 1000. The service reads current training bonuses and server settings, waits for stable backpack samples, applies the checked capacity and verifies readback. Only one capacity request can own a backpack at a time. An already modified capacity, including one set by an independent inventory mod, produces external_change; the service does not take over that change.

Slot changes use the reflected native SetBackpackSize function. Volume changes use the reflected capacity field followed by OnRep_MaxItemVolume, then verify the inventory, component and aggregate tracker values. This callback performs capacity synchronization, but it does not reproduce every bookkeeping operation in the game's unreflected native SetMaxItemVolume setter. The API does not claim native-setter equivalence; actual inventory use and save/reload remain part of release testing.

Stopping or reducing a capacity request keeps occupied slots and used volume accessible. A stop may report waiting / holding_items until normal capacity can be restored safely. It does not delete items to meet a lower limit. Unexpected changes outside the service pause automatic writes and restoration. Retain the handle until cleanup finishes, and explain any unresolved status to your user.

Handle operation Meaning
handle:status() Read a copied, cached result from any thread. Fields include state, code, message and the submitted revision; service-specific details may be present.
handle:update(options) Validate and queue a complete replacement of the same service request. Returns true, or nil, problem. A stopped handle cannot be reused.
handle:stop() Queue cleanup. Calling it again is harmless. stopped acknowledgement confirms service cleanup; queuing alone does not.
app:backpack_snapshot() Request a read-only report and return the last cached one. The first call may return ok=false, code='pending'. It never requests a capacity change.

A successful backpack snapshot includes current slots, volume, componentVolume, normalSlots, normalVolume, highestSlot, usedVolume and otherVolume. It may identify a service owner. This report contains plain values, not Unreal objects. It can become stale after the call; use it for diagnostics rather than as a write authorization.

The central runner accepts at most 32 client contexts and 8 service handles per context, with a 64 KiB message limit. Clients refresh their lease every second; the runner expires a missing client's requests after approximately 30 seconds of active game ticks and attempts the same safe cleanup. Object reconciliation coalesces repeated events; expensive service jobs yield between bounded units of work. Economy stays idle after its accepted changes complete until relevant changes request reconciliation. No game-object scanning occurs when there is no service demand. A paused game or stalled game thread can delay acknowledgements and cleanup.

Both services require checked reflected interfaces and a local authoritative Singleplayer session before writing. Backpack capacity additionally requires its verified private-layout profile. Scalar JSON messages cross Lua states; callbacks and Unreal objects never do. These checks coordinate participating consumers and detect some outside changes, but do not make arbitrary mods compatible. Restarting Dune remains the supported way to change installed scripts.

Service ownership is separate from app:claim and transaction resource claims. A generic claim does not reserve a merchant offer or backpack for these services, and service ownership does not block an unrelated direct write. Use the public service API for participating contributions; direct writers remain outside that coordination and may cause an external-change conflict. Reloading the Framework while service clients are registered reports restart_required instead of discarding their ownership records.

Scheduling and lifecycle

Call Result and use
app:defer(ms, callback) Schedule one game-thread callback. Returns a token with :cancel(). Delay is a positive integer, at most 86400000 ms.
app:retry(options) Bounded readiness polling. probe(attempt) returns a truthy value to on_ready(value). Returns a cancel token.
app:ready() On the game thread, returns true or nil, {code, message}.
app:session() Returns {ok, code, message, controller, pawn} for the current local session.
app:gate(scope) On the game thread, returns a verified session or nil, {code, message}. Checks the selected capability scope as well as session readiness; defaults to private.
app:find(path) Returns a valid loaded object or nil. Use on the game thread.
app:notify(classPath, callback) Observe newly created objects of a class. Return true to stop; token :cancel() silences later callbacks.
app:hook(path, callback, {kind='native', phase='post'}) Observe a loaded native UFunction after its original execution. Omit phase for the existing pre-call behavior. Returns a token or nil, error; cancelling unregisters the owned hook.
app:close() On the game thread, request service stops, cancel local work, restore transactions and release resource claims. Returns true, or nil, errors. Service restoration is asynchronous.

Retry options are interval_ms, max_attempts, probe, on_ready, on_timeout and optional on_error. Probe errors stop retries; terminal callbacks run on the game thread. Do not hold a character pointer indefinitely. Recheck app:session() after world travel, respawn or another event that may replace it.

A successful session result confirms an explicit BattlEye-off launch, the game's disabled BattlEye value, local player and character, solo play, and authoritative roles. A failed result explains why the feature should wait or remain inactive.

Object notifications may be registered at script load. Schedule game work with app:defer from callbacks when needed. Logical cancellation takes effect immediately; upstream releases a notification registration on the next matching event or mod unload.

The public hook API observes native UFunctions; callback return values do not replace the game's return value. Generic Blueprint, LoadMap, BeginPlay and EndPlay hooks remain outside this API. The current saved Economy stock profile binds three checked functions on its exact cooked widget to native implementations and observes its private native markers. It does not enable global script-dispatcher hooks or expand app:hook or the public Blueprint capability. Use bounded readiness checks and object notifications for public lifecycle work. Inspect app:capabilities() before depending on an optional service.

Settings

local store = app:settings {
    version = 1,
    defaults = { enabled = true, multiplier = 2 },
    fields = {
        enabled = { type = 'boolean' },
        multiplier = { type = 'number', integer = true, min = 1, max = 10 }
    },
    migrations = {}
}

if store.values.enabled then
    app:log('info', 'Multiplier: ' .. tostring(store.values.multiplier))
end
-- After an intentional settings change, call store:save() and check its result.

The store exposes values, status, error, path, :reload() and :save(). Missing files use defaults without writing. Unknown settings survive; missing known values receive defaults. Rules support type, integer, min, max, enum and max_length (UTF-8 bytes).

Settings live at Mods\WPS_Framework\settings\<ModID>.json under the launcher. The disk format is:

{"schemaVersion":1,"values":{"enabled":true,"multiplier":2}}

Increment the integer schema version when a migration is needed. Each migration is keyed by its old version and returns the next version's values:

migrations = {
    [1] = function(values)
        values.newOption = true
        return values
    end
}

Migration happens in memory until an explicit save. Invalid files, future versions and missing migrations are not overwritten. Saving stages a temporary file and retains a backup; an existing temporary file requires inspection before another save. Preserve the settings directory when updating the Framework.

Coordinate a verified change

Use a resource identity that describes the shared target, such as asset:/Game/...#Row.Field. Two mods that change the same target must choose the same identity. Do not add your mod ID to real shared target keys.

local tx, problem = app:transaction {
    name = 'Adjust a verified field',
    steps = { {
        resource = 'asset:/Game/YourAsset#YourRow.YourField',
        read = function() return read_current_value() end,
        write = function(value) write_current_value(value) end,
        value = desired_value
    } }
}

if not tx then
    app:status(problem.code, problem.message)
    return
end

-- Retain tx while the change should remain in effect.
-- When restoring it on the game thread:
local restored, recovery = tx:rollback()
if not restored then app:status(recovery.code, recovery.message) end

read_current_value, write_current_value and desired_value above are your own tested accessors and desired value. The Framework acquires all claims before any write, snapshots plain JSON-compatible values, applies changes and verifies readback. Each step can supply equal(before, after) for a deliberate tolerance or custom comparison.

If applying a step fails, touched steps restore in reverse order. A writer that changes the target and then raises an error is included in conditional recovery. If readback already matches the original value, rollback records the step as restored without calling its writer again. Otherwise the observed value must match the intended applied value before restoration; an external reaction is preserved and leaves recovery ownership retained. The returned error reports any failed restoration. A successful transaction retains ownership until rollback. Later rollback repeats these readback checks; an unexpected external change blocks restoration instead of being overwritten.

Unreal object wrappers and pointers are not suitable snapshots. Convert the fields you need to plain scalars or tables. For a compound operation with its own tested recovery, use app:claim(resource) directly; the returned token has :release(). Claims are re-entrant and reference counted within one client. A conflict returns nil, {code='conflict', message=...}.

Claims coordinate cooperating consumers that use the same claim identity. They cannot prevent unrelated mods from modifying game data and do not participate automatically in the merchant/backpack service registry. The Framework supplies no save-file editor; the game may persist effects of your tested accessors.

Checked property access

app:property(key, object) checks object validity and class, then returns a checked value. The two training attributes use reflected CurrentValue fields and type checks. The inventory-entry array and two tracker values additionally require the verified private profile and matching reflected anchors before registering a private descriptor. These are read-only helpers; they do not provide a general offset writer or modify global PropertyTypes.

Key Required object class Value
inventory.rows DuneTrainingModuleAttributeSet Current extra backpack rows
inventory.volume_bonus DuneTrainingModuleAttributeSet Current volume-capacity multiplier
inventory.items ItemInventory Verified 64-bit inventory entry array
tracker.max InventoryVolumeTracker Group maximum volume
tracker.used InventoryVolumeTracker Group used volume

The launcher and the kit's doctor command verify runtime file hashes. Lua does not independently fingerprint the loader. A game update requires a new profile audit and gameplay testing before new build-specific support can be claimed.

Status, events and cleanup

app:log(level, message) adds your mod ID and version, limits message length and suppresses consecutive duplicates. app:status(code, message) updates your status and emits a local status event; call app:status() to read it. app:report() returns local status, resource claims, settings health and cached service results, with current build evidence when called on the game thread. A requested backpack snapshot is included when one has been received.

Use app:on(event, callback) to subscribe and app:emit(event, value) for your own local events. Listener tokens support :cancel(). Built-in events are status and closed; events do not cross mod Lua states.

For service consumers, first call each handle's stop() and keep the context alive until each handle reports state='stopped'. Then call app:close() from a game-thread callback. A successful close() return confirms local cleanup and queued service stops; it does not confirm that the central runner has finished service restoration. Backpack cleanup can remain pending while occupied space exceeds normal capacity. Synchronous transaction or hook cleanup failures return errors and leave the context open for an explicit retry. Manual hot unloading cannot guarantee restoration before the runtime destroys a Lua state. Restarting the game is the supported update path.

Where to look next

Start with the kit's settings, lifecycle and table examples, then use the offline mock runner to exercise your own failure cases. The table example includes a complete apply/readback/rollback practice transaction on a private Lua record. Validate the real behavior in Dune before release.

The pinned UE4SS source defines the runtime beneath this API. Its Lua mod implementation and shared-variable implementation are the relevant references for module loading and cross-mod scalar coordination.

Independent UE4SS hooks

The Framework's app:hook wrapper observes loaded native UFunctions only; its capability flags describe Framework services. Independent mods can use UE4SS directly without a Framework dependency. Launcher 2.1.1 retains UE4SS 3.0.1-1140-gf58e8f84. The Launcher enables all 14 upstream hook settings for every selected mod and supplies Dune's checked virtual-table mappings: ProcessEvent 85, ProcessConsoleExec 88 and LoadMap 169. These settings are independent of WPS feature selections. The standalone Steam 25690505 test verified Blueprint/native callback delivery and unregistration; Microsoft PC 2.0.10.0 has static mapping evidence only. See the verification scope before declaring a feature tested.

Shared Economy, storage and scheduled work

The Framework supplies one shared runtime with independently checked Economy, stack and container-capacity capabilities. Consumers select the features they need; an unavailable capability does not disable unrelated services.

Economy selections

Use app:economy for the twelve stock categories, optional Sales and author-verified contributions. all=true requests every category and Sales; do not combine it with categories or an explicit Sales field. The retained request and handle signatures are unchanged.

Framework = require('../../WPS_Framework/Scripts/api')
local app = Framework.new {id='MyShop', version='0.1.0'}
local service = assert(app:economy {
    categories={'resources','schematics'}, sales=true, saved=true
})

saved=true requires the immutable selection prepared before launch. An explicit saved request does not fall back to a runtime table writer. Preparation, mounting and feature readiness have separate diagnostics. Game-version labels are advisory; the current interfaces and native body checks decide which features can operate. A mount or main-menu launch is not gameplay acceptance.

The saved repair keeps its pricing within managed merchant presentation and transactions. It does not require changing the game's global merchant demand setting. Original WPS base references and grade ratios remain separate from live item bases, so an independent base-price change scales the selected WPS policy consistently. Native preferred-vendor behavior, condition and character modifiers, rounding, currencies, stock, requirements and Sales minimum protection remain relevant. See Dune merchant integration for the distinct schematic multiplier and Economy reference-price contracts.

The saved widget filters the managed Exchange's cached source items by name, category, tier and grade. Ordinary merchants keep native controls and do not enter WPS search or grade-preview preparation. There is no separate verified item-level filter. Prepared ordinary input performs no repeated Lua item description, quote reconstruction or full-catalog scan. Source, world, offer ownership, policy and observed item changes trigger bounded invalidation. Native events enter the private bridge only after game-thread and lifetime admission; this does not expand public app:hook.

status() reports the acknowledged request revision. stop() queues withdrawal and must reach stopped before the app is closed. Loaded saved definitions and their required native guards remain until restart; independent owned contributions use checked restoration. Temporary session loss retains cleanup state without writing expired objects. A replaced world starts a new validated ownership epoch.

Generic consumers that omit saved retain their existing provider when no saved profile is requested. Its public contracts remain supported. With saved content, shared native ownership coordinates both Economy contributions and retained schematic offers; the service does not run a competing lossy map writer beside that journal. Arbitrary direct writes by other mods are outside service coordination and can produce a pending external-change diagnostic.

Author-supplied Economy stock

The retained contributions request accepts one to 64 author-verified entries with a qualified live table path, positive canonical price, stock, Solaris currency and explicit grades. New IDs do not need to appear in the shipped WPS catalog; they must resolve through the game's template lookup. Supplying a table path does not create or mount content. Malformed live entries are isolated, and a safely readable unsupported native grade retains native handling.

local shop = assert(app:economy {
    contributions={{
        id='YourVerifiedItem', table='/Game/YourMod/DT_Items.DT_Items',
        price=120, stock=0, currency='Solaris', qualities={0},
        exchange=true, sellable=false, category='custom'
    }}
})

price is an integer from 1 through 2,147,483,647; stock is 0 through 1,000,000, with zero unlimited. qualities is a dense unique list of grades 0 through 5. Optional qualityPrices uses string keys and covers every declared grade with positive integer prices. exchange defaults true; sellable defaults false and supplies eligibility only when Sales is requested. A Sell-only entry needs sales=true as well as exchange=false, sellable=true.

The service derives current base values and tags from live rows and verifies ownership at the exact offer, rather than treating a matching item ID as ownership. Agreeing owners can share a lease. A pre-existing matching offer is adopted read-only and left in place on withdrawal. External prices, stock and requirements are preserved. The Framework's internal ownerScope separates Economy and schematic leases; callers do not add it to public requests.

Accepted request syntax does not establish that its optional native policy is ready. Read status, retain waiting handles, and verify the displayed price, granted grade, wallet and stock in the game. The merchant reference, economy.schema.json and merchant.schema.json provide complete request details and lifecycle examples.

Backpack, base storage and vehicle cargo

Use Launcher 2.1.1 and Framework 1.7.1 for configurable whole-number multipliers from 1 to 1000. WPS Inventory 2.0.1 combines the three supported player features distributed on live Nexus: Backpack, Base Storage and Vehicle Cargo.

local backpack = assert(app:backpack_capacity { multiplier=7 })
local chests = assert(app:container_capacity { multiplier=100, scope='chests' })
local cargo = assert(app:container_capacity { multiplier=5, scope='cargo' })

The storage scope combines chests and cargo. Use either that combined request or separate chests/cargo requests. Disjoint scopes can have independent multipliers; overlapping claims report a conflict. Base chests multiply normal slots and volume. Vehicle cargo uses its checked native baseline, including the installed module. Backpacks retain earned bonuses and Singleplayer settings.

Capacity reductions protect occupied higher slots and volume. Retain each handle until restoration is acknowledged; holding_items can continue while stored contents need more room. Never change quantities or adopt another owner's conflicting limits. The four checked chest classes use the BuildingSubsystem's authoritative CDT_PlaceableData definition cache. Per-operation native validation remains required.

Framework 1.7.1 has checked native profiles for Steam build 26083551 and Microsoft PC package 2.0.15.0. Steam Inventory and Stacks Up passed actual world-map travel and a separate relog together; all 104 saved item identities, types and quantities were retained. Xbox four-mod startup and independent live checks of 82 native ranges and three vtable bindings passed. Xbox loaded-world gameplay remains unverified. Native bodies, current object identities and reflected interfaces remain independently checked; installation labels alone never authorise private access.

Cancellable scheduled jobs

local job = app:job {
    name='Inspect inventory',
    log=true,
    run=function(task)
        local result={}
        for _, name in ipairs(names) do
            result[#result+1] = task:engine(function()
                return inspect_one_loaded_object(name)
            end)
            task:checkpoint()
        end
        return result
    end,
    on_done=function(result) app:log('info', 'Inspection complete') end,
    on_error=function(reason) app:status('inspection_failed', tostring(reason)) end,
    on_cancel=function() end
}
-- job:cancel()

run receives a task in a planning coroutine. Use task:engine(fn, ...) for every UE4SS or Unreal call: it suspends the planner and executes the operation from the registered Lua state's game-thread callback. Direct engine calls from the planning coroutine are unsupported by the pinned runtime. Return values or an error are delivered to the coroutine on its next turn.

task:checkpoint() yields after bounded pure Lua work. Keep individual units small; no scheduler can preempt a single synchronous engine call. The runner targets 2 ms per callback, with subsequent callbacks deferred approximately 16 ms. It shares work fairly among up to 64 jobs per context and limits the number of units per callback. Framework-owned services use the Framework context's common queue. Separate author contexts have separate budgets, so authors should avoid duplicating world-wide work.

A token exposes state, active, result, name and metrics (calls, total engine-call seconds, maxCall, maxCallSource and terminal elapsedSeconds). The optional job name is at most 64 characters and cannot contain control characters; the token adds a unique job number. log=false suppresses routine completion metrics for that job without changing other jobs' reporting. States include pending, running, complete, error and cancelled. Cancellation prevents future scheduled steps; it does not roll back mutations already performed. A service that changes game data must retain a verified recovery journal and schedule safe restoration. Closing a context cancels its jobs. Completion and error callbacks run through the registered callback; callback failures become diagnostics.

Measure large engine calls, avoid repeated whole-world scans, cache only validated identities, and refresh the inventory affected by an event. Frame-time capture and actual save-load runs remain necessary to assess player-visible stuttering.

Create content for Dune

Copyright (c) 2026 Wasted Potential Studios. All rights reserved.

The creation tools let your mod define its own rows, buildable variants and assemblies of existing game meshes. Use Framework 1.7.1 with the Launcher’s UE4SS 1140 runtime. Your mod keeps its own permanent ID and declares the Launcher and Framework as separate dependencies. Players do not need the Modder Kit.

Framework and Modder Kit 1.6.7 provide public creation services for independent compatible mods. Steam 25789117 and Microsoft PC 2.0.11.0 have static native body, layout and admission verification. Loaded-game registration, typed field changes, composition, placement, progression, save/reload and automatic placeable reconstruction remain unverified for this release on both editions. Test your creation in a separate local world before using it in an established save.

WPS Dune Launcher 2.1.1 shows available updates beside each mod version.

WPS Dune Launcher 2.1.1 shows available updates beside each mod version.

Choose the right lifetime

Your content Public API Lifetime
A new buildable or item definition derived from an existing game row app:creation() Definitions are discovered before loading a world and retained for the process lifetime once committed.
Meshes arranged around an existing actor, such as a display or visual attachment app:compose(anchorResolver, {parts = {...}}) The Framework owns the spawned actors and removes them when stopped or when their anchor/world retires.

A composed display is not automatically a buildable, inventory item or saved object. A creation definition can attach visual props to a registered placeable. Those props use the same local transform and material format as a transient composition. Structure pieces currently support row-defined mesh changes. Their props list must remain empty because individual structure-piece attachment and removal have not been verified; use a separate composition only with a verified actor resolver.

The tools use assets that the running game can resolve. Custom mount roots are accepted when their assets are available. A mesh object path does not mount a content container or import a modelling file. Unreal cooking and container compatibility remain separate from these APIs.

Start a creation project

Choose creation in the Kit’s Create view, or use:

python kit/wps.py new C:\DuneMods\MyBuildable --id MyBuildable --name "My Buildable" --author "Your Studio" --description "A new buildable for Arrakis" --template creation --edition both

The project contains:

Mods/
  MyBuildable/
    mod.json
    Creation/
      defs.json
    Scripts/
      main.lua

Edit Creation/defs.json. The generated Lua script activates the installed document through app:creation() and reports its status. There is no generated Lua copy of the JSON to keep synchronized. The editor associates the file with the Kit’s creation schema, and both Check project and Build release ZIP validate it.

The starter defines MyBuildable_Floor by copying Choam_Shelter_Floor_New and changing its display name. It retains the existing mesh, Floor group, Tier0, CHOAM faction and Floor socket setup. The existing ChoamShelterSet unlock and Floors category give it a concrete starting point. The example keeps its template's linked cost and placement data; it does not duplicate those tables. The running game must resolve those source links. A new generated mod needs its own placement, progression and save/reload checks on each supported edition.

The generated project declares both PC editions without claiming that your new mod has passed a game test. Keep only the editions you intend to test and support.

Give content a permanent identity

The document’s owner must exactly match the mod’s manifest ID and Framework App ID. Each row ID starts with that owner followed by an underscore, such as MyBuildable_Platform. Keep published IDs stable while saved worlds use them.

Rows are identified by their table and ID together. The same ID can appear in different linked tables; duplicate table/ID pairs, including case-only differences, are rejected. A buildable entry must reference an exact row ID declared in its own document. Source templates must already exist before the batch begins.

Definition field Purpose
schemaVersion Use 1.
owner Your permanent mod ID.
rows One to 64 new row definitions. Each supplies id, table, source and optional fields.
table The full DataTable object path containing the source template.
source An existing row in that table. The new row starts as a copy of it.
fields Only the reflected properties you want to change on your new row. Unspecified properties retain the template’s values.
buildables Up to 64 build-menu integrations with row, kind, unlock, category and optional placeable props.
kind structure or placeable; native validation checks the appropriate building table.
unlock An existing buildable unlock-set row. This uses the game’s existing progression.
category An existing build-menu category containing the inherited buildable group, or receiving a group cloned under this document's ownership.

The Framework applies changes to the new rows. It does not turn a new row definition into permission to overwrite its source game row or another author’s content.

Menu categories list buildable groups, not individual building row IDs. The starter inherits the Floor group, which is already in the Floors category. Keep that relationship when copying a template. To define a separate group, clone its group row under your own ID and reference that group through the building row's reflected fields. The native checks distinguish an owned new group from a shared existing group and preserve shared membership.

Describe reflected values

Use JSON booleans, finite numbers and strings for scalar properties. Structs use nested objects; arrays and reflected sets use JSON arrays. Reflected maps use JSON objects when their keys are strings, names or single-name handles. The native implementation checks the actual key, element and value types against loaded metadata. An object reference uses an explicit asset tag:

{"asset": "/Engine/BasicShapes/Cube.Cube"}

That tag describes an object reference. It is not a substitute for a string or row name when the target property expects one. Property names, structure layouts, numeric types and referenced objects must match the actual loaded game metadata. The native implementation checks them before committing a definition.

The Kit checks shape, ownership and bounds without running your mod. It deliberately does not guess whether a property exists in the selected game build. A syntactically valid definition can therefore receive a runtime diagnostic for a missing template, an unexpected property type or an unavailable asset.

Build costs, recipe outputs and snap layouts can depend on other rows. Clone the needed linked definitions under your own IDs and point your new content at those IDs through the correct reflected fields. Copying a template preserves its original links until you change them. A changed visual mesh alone does not update its recipe, footprint, placement tests or snap rules.

Compose meshes and materials

The composition starter runs a brief visual example: a miniature Choam shelter floor above the local character, with collision disabled. It removes its owned actor after fifteen seconds and checks the cleanup result. Create it with the same command as the creation starter, replacing --template creation with --template composition and choosing a new project folder and Mod ID.

Each prop has a unique id and a full mesh object path. Transforms are local to the anchor. Optional values have these defaults:

Field Default Meaning
location { "x": 0, "y": 0, "z": 0 } Local position in centimeters.
rotation { "pitch": 0, "yaw": 0, "roll": 0 } Local rotation in degrees.
scale { "x": 1, "y": 1, "z": 1 } Signed scale on each axis.
collision false Whether to request collision for the owned part.
materials [] Material object paths in slot order; the first entry addresses slot zero.

There can be up to 32 parts, with up to 16 material entries per part. Position and rotation components are bounded to ±10,000,000. Scale magnitudes must be between 0.0001 and 1,000; negative values are allowed. Colliding props still depend on the mesh’s collision data and the game’s checked collision interfaces.

Each App can own up to 16 simultaneous compositions and 512 spawned actors. Exceeding that pool returns composition_limit. Actors awaiting failed cleanup remain owned and count toward the pool until cleanup succeeds.

For transient composition, pass a function that resolves the same live anchor actor in the current world. Keep the returned handle and inspect handle:status(). handle:stop() destroys the actors owned by the composition, not its anchor. Do not save raw Unreal object handles or reuse them across a world transition.

Activate and inspect

The native Framework discovers installed Creation/defs.json documents before world loading. Calling app:creation() activates the document belonging to the calling App ID. Supplying app:creation(spec) additionally asserts that the supplied definition matches the installed immutable document; it is not a late row-creation or hot-reload route. Restart the game after editing an installed definition.

Definition registration and owned local menu activation are startup data operations; they do not require a player session. Spawned compositions and reconstructed placeable props require a local authoritative Singleplayer session. Definitions remain loaded when that session ends. This distinction does not establish multiplayer compatibility or make the definitions available to a remote server.

The JSON document uses [] for empty lists. An optional Lua spec can use {} in declared list slots; those empty Lua tables normalize to lists before comparison with the installed document.

The returned handle provides status() and stop(). Status describes the owner's document as one batch; details.rows is its row count, not an array of individual row results. Inspect the reported state and diagnostic message. Acceptance of a request does not prove that every template and asset was available or that the game accepted the content.

An App keeps one creation handle. After stop(), another app:creation() call on that same App returns the stopped handle; it does not reactivate the document. Await the stopped acknowledgement before closing the context, and plan activation as part of the mod's session lifecycle.

The Framework checks installed definitions before a world loads, including those whose mod is disabled. Invalid definitions are rejected, and a startup registration failure can leave the creation batch unavailable. World loading continues and other Framework functions remain available. Read Mods/WPS_Framework/dlls/creation.log for the affected definition and reason. Correct the definition, or restore its working version, then restart to register it. Keep the content installed while a saved world refers to its IDs; deleting those files is not a repair for a world that uses them.

Stopping the handle hides its owned buildable rows, removes its owned group membership from the requested menu categories and stops its reconstructed props. Committed definitions remain for the process lifetime because saved objects may reference them. Installed disabled definitions also remain discoverable for those saved references. This retention is not an uninstall migration. Keep the definitions installed until affected worlds no longer contain or reference the content.

Check and test

From the Kit directory:

python wps.py creation C:\DuneMods\MyBuildable
python wps.py lint C:\DuneMods\MyBuildable --require-syntax
python wps.py package C:\DuneMods\MyBuildable --out C:\DuneMods\MyBuildable.zip --require-syntax
python wps.py api --key framework.creation
python wps.py api --key framework.compose

For a standalone JSON document, use creation path\defs.json --owner MyBuildable. The checker rejects unknown document fields, invalid owner prefixes, conflicting identities, unresolved local buildable references, duplicate reflected field names, null values and out-of-range data. Normalized documents are limited to 512 KiB; fields permit 12 nesting levels, 4,096 total nodes and 256 entries per object or array. Strings are limited to 8,192 UTF-8 bytes. Finite field numbers are bounded to ±1,000,000,000,000. The runtime repeats validation before inspecting game data. Startup discovery also has shared limits of 256 owners and 4,096 rows across all installed creation documents.

Use a separate release world for the first placement. Check the intended unlock, menu category, cost, snapping, footprint, appearance and dismantling behavior. Save, exit and reload with the same definitions installed, then test disabled activation while retaining the installed definition. Verify every edition you declare.

Package, schema and source checks do not establish gameplay acceptance or save recovery. Publish only the behaviors your own finished mod has demonstrated.

Item stack limits

app:stack_limits{preset=2} requests the Framework's shared stack service. Framework 1.7.1 accepts integer factors from 1 to 1000, or 'maximum'. Any mod author can use this service with its own consumer ID. WPS Stacks Up 1.1.1 provides one configurable download, with a custom multiplier or Maximum in the Launcher settings panel. Install Launcher and Framework separately and retain the consumer package's declared dependencies. The consumer package must not contain another copy of the Framework or Launcher.

Framework = require('../../WPS_Framework/Scripts/api')
local app = Framework.new {id='MyStackMod', version='1.0.0'}
local stacks, problem = app:stack_limits {preset=5}
assert(stacks, problem and problem.message)
app:every {interval_ms=2000, callback=function()
    local status = stacks:status()
    app:status(status.state, status.message)
end}
-- A consumer can request a fresh check with its unchanged preset:
-- local accepted, reason = stacks:update {preset=5}
-- assert(accepted, reason and reason.message)
-- Inspect the acknowledged revision and state; submission alone does not
-- confirm that every loaded item has been refreshed.

The Framework owns the request, game-thread scheduling, authoritative Singleplayer checks, item-table transactions, existing-item refresh and restoration. There is one active stack policy. A competing request reports owner_conflict; factors never multiply one another. update{preset=10} recalculates from the captured normal limits. An accepted update with the same preset also requests one bounded revalidation of owned definition limits and loaded item caches. stop() queues restoration; continue polling until state='stopped'. Item quantities are never changed by the service. The service applies the same lifecycle and ownership rules to every consumer ID.

Only item definitions whose normal StackAndDurability.MaxStackSize exceeds one qualify. Definitions with zero or one remain unchanged. The service changes only maximum stack counts, leaving durability, decayed durability, upgrades, qualities and other item state intact. It does not add stacking to equipment or replace the game's merge, split or item-matching rules.

Existing inventory items

Changing CDT_BaseItems alone changes newly created stacks but leaves existing stacks with their cached limit. The inventory's merge checks read the effective maximum from the item's stack-and-durability component. The Framework therefore updates both the base and effective maximum of loaded stackable items through a checked native operation. It preserves the remaining component bytes and verifies the item quantity. After initial discovery, newly created inventories are queued by object notifications and checked for initial content readiness. Notifications for the same inventory are coalesced. An inventory or item that is not ready remains queued instead of being silently abandoned; retry delays rise through 1, 2, 4, 8, 16 and 30 seconds and remain capped at 30 seconds. A settled request performs no recurring full inventory scan. Newly created items inherit the changed definition through the game; particular late-content delivery paths still require their own runtime acceptance.

Checked native inventory-content signals also queue the affected item identities. The service coalesces those IDs, then verifies their world, database, template and expected limits before refreshing them directly. This targeted work does not enumerate an inventory array or perform a global scan. A queue reset or overflow schedules one bounded discovery pass. The native event capability is required for stack activation; consumers continue using stack_limits, update, status and stop without a separate public inventory-event interface.

The native catalogue includes Steam builds 25486029, 25611704, 25690505 and 25789117 and Microsoft PC packages 2.0.10.0 and 2.0.11.0, using UE4SS 3.0.1-1140-gf58e8f84. Complete native bodies, scope constructors, an engine-context caller and current reflected interfaces authorize each operation. Version labels and whole-executable identity are advisory. The native module belongs to the separately installed Framework; stack consumers remain Lua-only. An unavailable native capability leaves stack changes inactive while other Framework capabilities remain independently checked. Inspected code does not establish every inventory, transition or restoration path's gameplay behavior.

The service captures normal table limits once, checks its owned values before updating them, and pauses if another writer changes an owned maximum. Unexpected per-item modifiers also block refresh. A table replacement reports a conflict and requires a game restart. Cooperating authors should use this service instead of writing these fields independently.

A changed or unproven write outcome remains a conflict after readback. An unrelated inventory-content event does not clear a genuine conflict. Resolve its cause before requesting another explicit revision; an update is not authorization to overwrite someone else's values.

Scheduling and runtime state

The service splits work across game-thread callbacks, yielding after a 2 ms budget or 128 work units. An individual engine operation remains atomic and may exceed that budget. Status remains waiting during preparation and becomes active after the requested readback completes. New requests, accepted updates, stopping, and newly ready inventories queue work. Idle reconciliation checks the current session, controller, pawn, checked native world and top-level item-table identity; it does not repeatedly enumerate inventories. Each native item operation checks the current pawn/controller relationship, local authority, Singleplayer, game thread and BattlEye state before accessing the item.

A verified world change releases claims for retired item identities without writing those old IDs. If the same owned item table remains, its definitions stay owned and the new world's loaded items receive one validation pass. A replaced item table reports a conflict before writes. Temporary session unavailability retains the request and unfinished recovery; a stop is complete only after its acknowledged state becomes stopped. Another consumer cannot take over retained ownership early.

The installed UE4SS build cannot dispatch its engine APIs directly from a Lua coroutine: it reports that the coroutine Lua state has no registered instance. The stack service uses a coroutine only to plan pure Lua work; every engine operation executes through the original registered game-thread callback. Authors must not call UE4SS engine bindings from a coroutine. Use task:engine inside app:job, or app:defer for a single engine operation. Repeated inventory notifications are coalesced, and inventory refresh does not repeat whole item-table validation.

Maximum and practical limits

The maximum count fields are signed 32-bit integers: 2,147,483,647 is the representable ceiling. Multipliers clamp to that ceiling. Inventories, transfer dialogs, recipes, vendors and user-interface counters may have additional practical limits. Item volume, slots and other game rules still apply. Long counts can exceed the space provided by the inventory UI.

The Steam build 25486029 table capture counted 4,203 definitions, of which 215 had a normal maximum greater than one. These include disabled and deprecated definitions; the counts do not describe obtainable items or establish the current build's live table count.

Stopping the service restores limits without deleting or splitting items. Reduce oversized stacks before selecting a smaller preset or removing the mod; quantities above a restored limit may require splitting before further merging. Restoring a component maximum does not restore an earlier durability snapshot.

Author tools

Search the Modder Kit or website reference for stack, stack_limits, MaxStackSize, durability or maximum. python kit/wps.py new --help includes the stacks consumer template. Use python kit/wps.py api --key framework.stack_limits for its options and support boundary. The template preserves the requested Steam, Microsoft or both-edition selection and declares both foundational dependencies. Generated projects do not impose a game-version whitelist; authors must test their own behavior on each chosen edition.

Item snapshots and copied row queries are not complete item-state clones. They cannot establish exact equality of every equipment modifier or safely implement new equipment stacking. Native generated component wrappers contain additional state beyond their reflected fields; a plain Lua table of visible stats is not a replacement for that state.

Reusable mod infrastructure

Framework 1.7.1 provides managed scheduling, player lifecycle observations, checked scalar patches, independent data stores, keybinds, console commands and communication between mods. These services are optional: an unused context creates no infrastructure polling. Installed creation definitions are registered before world loading to preserve their saved identities; their consumer activates presentation separately. Other gameplay services act on explicit consumer requests.

The shared broker wakes for active service requests or messaging work. A subscription, advertised provider or pending request keeps the bounded lease clock running; releasing the last such handle lets it sleep. Publishing without any active messaging peers requests one clock pulse to replenish the message budget. Completed service cleanup also stops its client's heartbeat. Explicit scheduling and player watchers keep their own requested timing and cancellation behavior.

Import the installed dependency and create one context for your Mod ID:


Framework = require('../../WPS_Framework/Scripts/api')

local app = Framework.new {id='MyMod', version='1.0.0'}

Use a different ID for each mod. Creating another context with the same ID replaces its shared identity; callbacks from the earlier context cannot continue using shared services. Keep the same context for the lifetime of your mod. The launcher manifest must declare the Framework dependency when you import this API.

Most new calls return a value or handle on success, or nil, {code, message} on failure. A callback exception is contained and reported through app:status(). app:report() includes active infrastructure handles, data-store status, subscriptions, providers, pending requests and input registrations.

Saved merchant widget interface

Framework 1.7.1 combines the cooked merchant widget with a checked compiled query function at the managed Exchange. Search, category, tier and grade events use cached item metadata and preserve original item objects in the game's sort order. Ordinary merchants keep their native Buy and Sell controls. Prepared input performs no Lua row scans or price queries; source or policy changes invalidate the affected cache.

This is a checked uiContract=2 profile capability supplied by Framework 1.7.1 and Launcher 1.2.13. Request it through the public app:economy{categories={'resources'}, saved=true} service with the selection prepared before launch. The Framework binds only WPS_Query, WPS_PrepareResult and WPS_ContentChanged on the checked merchant class. Native events require game-thread admission and checked weak window/world identities before Lua is entered. Closing invalidates that opening; reopening needs a fresh lifetime. These internal bindings do not expand the public app:hook contract or require global script-dispatcher hooks.

The Framework's app:hook wrapper observes loaded native UFunctions only; its capability flags describe Framework services. Independent mods can use UE4SS directly without a Framework dependency. Launcher 2.1.1 retains UE4SS 3.0.1-1140-gf58e8f84. The Launcher enables all 14 upstream hook settings for every selected mod and supplies Dune's checked virtual-table mappings: ProcessEvent 85, ProcessConsoleExec 88 and LoadMap 169. These settings are independent of WPS feature selections. The standalone Steam 25690505 test verified Blueprint/native callback delivery and unregistration; Microsoft PC 2.0.10.0 has static mapping evidence only.

The saved query accepts up to 16,384 source items and 256 query characters. Name, category, tier and grade are supported; a separate item-level field has not been verified. An incompatible contract reports its error without falling back to polling.

Stopping the service withdraws its callbacks and pauses attached widgets; changing loaded definitions requires a restart. Checked signatures, source fixtures and headless widget results do not establish buying, selling, save/reload or whole-game frame-time acceptance. Acceptance remains specific to the package, settings and operations exercised in each test. See the shared Economy contract for selections, ownership and transaction safeguards. The Kit lookup key is economy.saved_widget_native.

Scheduling and lifecycle

| Method | Arguments and result |

| --- | --- |

| app:every(spec) | interval_ms: integer 16–86,400,000; callback(run): required; max_runs: optional integer 1–1,000,000. Returns a managed handle. |

| app:debounce(key, ms, callback) | Replace the pending callback for this key. Delay is 1–86,400,000 ms. Returns a managed handle. |

| app:throttle(key, ms, callback) | Schedule one leading callback and reject another call with the same key until the 16–86,400,000 ms cooldown ends. Returns a handle or nil, problem with code throttled. There is no trailing callback. |

| app:watch_player(spec) | Required callback(event) and optional interval_ms of 250–60,000, default 1,000. Returns a managed handle. |

| handle:cancel() | Cancel the pending job and release its managed slot. Repeated cancellation is harmless. |

| handle:status() | Returns {active, runs, error?}. runs counts executions, including a callback that fails. |

There are at most 64 active managed scheduling and watcher handles per context. Cancelled and completed handles release capacity. Keys start with a letter and use letters, digits, _, ., :, / or -, up to 96 characters. Invalid arguments return invalid_schedule or invalid_watch; exceeding capacity returns schedule_limit.

Callbacks run on the game thread. every waits the interval after each callback finishes, so slow work does not create a backlog of missed ticks. Return false to stop; an exception also stops that recurring handle. Calling app:close() from the callback prevents rescheduling. Debounce and throttle keys are local to the current mod context.


local refresh = app:every {

    interval_ms=1000,

    max_runs=60,

    callback=function(attempt)

        local snapshot, problem = app:player_snapshot()

        if not snapshot then return end

        app:log('info', 'Player snapshot refreshed on attempt '..attempt)

        -- Update your own display from the copied snapshot.

    end

}



local function searchChanged()

    app:debounce('catalog-search', 200, function()

        -- Run the latest search once the input settles.

    end)

end

watch_player resolves the current local controller on each poll. It emits only a transition, not a callback on every unchanged sample:

| event.kind | Meaning |

| --- | --- |

| waiting | No eligible session is available. A changed failure code can produce another waiting event. |

| ready | A local authoritative Singleplayer character became available. |

| replaced | A ready controller or pawn has a different object address. |

| lost | The previously ready character became unavailable or ineligible. |

Each event includes generation, which increases on ready/replaced, and session, the fresh local session result. current and previous, when present, are plain {controller, pawn} address pairs used only as identity markers. Do not treat those numbers as memory-access permission. The watcher retains no old Unreal object; a callback should also avoid retaining the prior pawn. The session watcher does not itself grant permission to write: use the appropriate compatibility scope and a checked change API at the time of the write.


local playerWatch = app:watch_player {

    interval_ms=1000,

    callback=function(event)

        if event.kind == 'ready' or event.kind == 'replaced' then

            app:log('info', 'Local character generation '..event.generation)

        elseif event.kind == 'lost' then

            -- Clear your own character-specific display and cached plain data.

        end

    end

}

Checked reflected scalar patches

app:patch(spec) applies one reflected scalar property through the existing transaction system. Prefer a named engine_change operation when one exists: its setter and bookkeeping are already described by the engine adapter. patch is for an author who has verified an additional reflected property and its update requirements.

| Field | Requirement |

| --- | --- |

| resource | Shared logical target/property identity, 1–512 printable bytes. All cooperating authors touching the same field must agree on this identity. |

| resolve(session) | Return the current target object. Called again before each read, write and restoration. |

| class | Loaded reflected class path for the target, at most 512 bytes. |

| property | Reflected scalar field name, at most 128 characters; letters, digits and underscores, beginning with a letter or underscore. |

| value | Finite number, boolean or string. Its Lua type must match the current reflected value. Maximum encoded scalar size is 4 KiB. |

| changed(object, phase) | Optional callback after assignment. phase is apply or rollback. Invoke any verified setter/notification required by that specific field. |

| name | Optional transaction label; defaults to the property name. |

The result is the normal transaction handle: tx.state, tx.errors, and tx:rollback(). The patch checks the reflected session route, local authority, property type, target validity, class and original object address before every access. A different object from the resolver is refused, including during rollback. Readback must equal the requested value. Rollback checks that the field still equals the value applied by this transaction, so a later external change is preserved and reported.

The following helper illustrates an author-owned, unbound progress bar. Supply a resolver for that exact widget; a game-owned bound widget may have its own update rules. The callback calls the supported UMG setter to refresh the widget after assigning the reflected field.


local function setOwnProgress(resolveBar)

    app:defer(1, function()

        local tx, problem = app:patch {

            resource='ui:MyMod.StatusProgress#Percent',

            class='/Script/UMG.ProgressBar',

            property='Percent',

            value=0.5,

            resolve=resolveBar,

            changed=function(bar, phase)

                bar:SetPercent(bar.Percent)

            end

        }

        if not tx then app:log('warn', problem.message); return end

        app:defer(5000, function()

            local restored, why = tx:rollback()

            if not restored then app:log('warn', why.message) end

        end)

    end)

end

Argument, reflection and type failures return invalid_patch; ordinary context/build/session failures return their existing gate code. A cooperating owner produces conflict. A failed application with successful restoration returns apply_failed. Failed restoration returns rollback_failed, retains the claim and tracks the recovery transaction for app:close() to retry. Inspect the returned problem and leave the context loaded when cleanup is incomplete. No API can safely restore an object that has been destroyed or replaced. The patch helper does not infer native side effects or create an unreflected property descriptor.

Namespaced persistent data

app:data(name, spec) opens a separate local store for author data beyond the mod's main configuration. name follows Mod ID characters: letter first, then letters, digits, underscores or hyphens, up to 64 characters. Each mod may open eight names. Reopening a name returns its existing handle; requesting a different schema version for that open name returns schema_conflict.

The file is WPS_Framework/settings/data.<ModID>.<name>.json. It is local to the installed launcher mod folder, not automatically associated with a character, Steam account or cloud save. For character-specific data, store a game identifier that you have independently verified; an in-memory object address is not a persistent character ID. Opening, reading or editing a store in memory does not write a file.

The schema uses the same migration format as app:settings:


local progress, problem = app:data('progress', {

    version=2,

    defaults={visits=0, notes={}},

    fields={visits={type='number', integer=true, min=0, max=100000}},

    migrations={

        [1]=function(old)

            old.visits=old.count or 0

            old.count=nil

            return old

        end

    }

})

assert(progress, problem and problem.message)



app:defer(1, function()

    local changed, why = progress:update(function(values)

        values.visits=values.visits+1

    end)

    if not changed then app:log('warn', why.message); return end

    local saved, why = progress:save()

    if not saved then app:log('warn', why.message) end

end)

| Method | Behavior |

| --- | --- |

| store:get() | Return a copied table of all current values. |

| store:get(key) | Return a copied value for a key, or nil if absent. Invalid key syntax returns nil, problem. |

| store:set(key, value) | Validate and update memory. Setting nil removes a key; a declared default is restored during validation. |

| store:update(callback) | Pass a private candidate table to the callback. Mutate it or return a replacement object. Commit memory only after the callback and validation succeed. |

| store:save() | Explicit game-thread commit after app:ready(). Reject an unexpected on-disk change instead of overwriting it. |

| store:reload() | Re-read and migrate the disk copy, discarding unsaved local edits on success. Reject invalid/newer data and concurrent changes while reading. |

| store:status() | {state, error?, namespace, schemaVersion, dirty}. States include defaults, loaded, saved, invalid and read_failed. |

version is an integer 1–100,000. defaults is required. Values have an object root and plain JSON content, limited to 256 KiB encoded, 2,048 visited values and depth 16. Field rules support type, numeric integer/min/max, string max_length, and scalar enum. Unknown fields are retained. Migration [n] takes a copy of version n's values and must return the next version's values; each intermediate migration is required.

Newer schemas, malformed content, failed migrations and unreadable files are preserved. Saves use the existing temporary-file/backup workflow. external_change means reload and merge before saving. This is an optimistic conflict check, not an interprocess file lock: do not edit the file simultaneously with a save. Other common failures are invalid_store, store_limit, invalid_key, invalid_callback, invalid_data, read_failed, save_failed, wrong_thread and closed. A closed handle remains readable but cannot update, reload or save.

Events between mods

app:on and app:emit remain local events within one context. Use subscribe and publish for messages between separate Lua mods:


local listener, problem = app:subscribe('MyPack.settings.changed', function(value, meta)

    app:log('info', meta.sender..' selected '..value.preset)

end)

assert(listener, problem and problem.message)



app:defer(1000, function()

    local sequence, why = app:publish('MyPack.settings.changed', {preset='quiet'})

    if not sequence then app:log('warn', why.message) end

end)

app:subscribe(topic, callback) returns an active handle with :cancel(). Registration may occur at script load; its first game-thread poll establishes its starting point. app:publish(topic, payload) must run on the game thread and returns the shared sequence number or nil, problem. The callback receives its own copy of payload plus {topic, sender, sequence}. Publishing a message does not synchronously invoke subscribers or prove that any subscriber consumed it. A matching subscriber in the publisher's own context can also receive the event.

Topics use the same bounded 96-character names as scheduling keys. Payloads are finite numbers, booleans, strings, dense arrays, string-keyed objects, or the Framework JSON null marker. Nil, functions, engine objects, sparse arrays, cycles and arbitrary metatables are rejected. Each payload is at most 4 KiB encoded, depth 16 and 2,048 visited values. Empty ordinary Lua tables represent objects; use the bundled JSON module's array marker when an empty array matters.

There are at most 64 subscriptions per context. Each context may append 32 messages per 250 ms Framework tick. All contexts share a 128-message ring; a consuming poll handles at most 64 entries before yielding. The ring intentionally bounds memory, so events are best-effort. A client that falls behind receives transport_overflow through app:on('ipc_error', callback) and app:status(), then reads the retained messages. There is no replay for a newly started subscription and no disk persistence. Keep payloads small and publish state changes rather than per-frame data.

Service discovery and request/reply

An author can offer a reusable service without exposing its Lua callbacks to another mod:


local service, problem = app:provide('MyPack.presets', function(request, meta)

    if request.operation ~= 'list' then

        return nil, {code='invalid_request', message='Use operation=list.'}

    end

    return {names={'quiet', 'balanced'}, owner=app.id}

end, {version='1.0.0'})

assert(service, problem and problem.message)

app:provide(name, handler, {version}) returns a handle with :cancel() and :status(). The status includes active, advertised, name and version. Advertising happens asynchronously on the next game-thread poll. Service versions must be major.minor.patch; a context can provide 16 distinct names. A handler receives a copied request and {sender, service, version, request}. Return one plain result, including false if appropriate, or nil, {code, message} for a structured failure. A handler error becomes handler_error; a missing or non-plain result becomes invalid_reply.

app:discover(name) runs on the game thread and returns a sorted array of {owner, name, version} for currently advertised providers. An empty array is a valid result. Each provider context uses one of 32 shared provider slots and renews a 30-second lease while it has services. A closed/replaced context or expired lease disappears from discovery. Available slots are not reserved until the advertising poll; provider_limit is reported if all slots are occupied.


app:defer(1500, function()

    local providers, problem = app:discover('MyPack.presets')

    if not providers or not providers[1] then return end

    local peer = providers[1]

    local request, why = app:request(peer.owner, peer.name, {operation='list'}, {

        version=peer.version,

        timeout_ms=5000,

        callback=function(result)

            if result.state == 'complete' then

                app:log('info', 'Available presets: '..table.concat(result.value.names, ', '))

            else

                app:log('warn', result.error and result.error.message or result.state)

            end

        end

    })

    if not request then app:log('warn', why.message) end

end)

app:request(owner, name, payload, options) requires the game thread. owner is the provider Mod ID; name is the service name. Options are timeout_ms (100–60,000; default 5,000), optional exact version, and optional callback(result). A missing provider returns service_unavailable; a requested version mismatch returns version_mismatch. The request captures the provider's current context identity and advertised version, so a replacement provider cannot answer as the earlier provider.

The returned handle has :status() and game-thread :cancel(). Status is a copied result with state equal to pending, complete, error, timeout or cancelled. Complete results contain value, owner and service. Failures contain an error with code/message when available. Explicit cancellation includes owner/service. The completion callback runs at most once per handle and is protected against exceptions.

There are 32 pending requests per context. A provider retains monotonic request counters for up to 32 live requester identities to reject duplicate execution; replacing the requester context releases its replay slot. A response has the same 4 KiB plain-value limit as an event. Provider handlers execute on their own game-thread poll; they must return promptly and must not yield or wait for another reply. Make lengthy workflows explicitly asynchronous at the application level.

Timeout or cancellation ends the local wait. It does not undo a request that the provider has already handled, nor guarantee that a queued remote handler will never run. Design state-changing services with application-level idempotency and queryable status. There is no automatic retry. Ring overflow ends pending requests with transport_overflow; provider replacement produces provider_replaced. Corrupt or oversized envelopes are rejected as invalid_message or invalid_transport. Other bounded failures include rate_limit, request_limit, requester_limit, subscription_limit, service_limit, duplicate_service, invalid_request, invalid_service, invalid_subscription, invalid_topic, invalid_value, replayed_request, sequence_limit and transport_unavailable.

This transport coordinates cooperating mods. All installed Lua mods already have runtime access, so these identity checks are lifecycle/conflict protection, not authentication against a hostile installed mod. No UObject, native pointer, callback or mutable Lua table crosses the state boundary; only encoded strings use the runtime's shared-variable API.

Managed keybinds and console commands

Register input helpers on the game thread after the Framework is ready:


app:defer(1000, function()

    local key, problem = app:keybind {

        key=119, -- F8

        modifiers={17}, -- Control

        callback=function(binding)

            app:log('info', 'Control + F8 pressed.')

        end

    }

    if not key then app:log('warn', problem.message) end



    local command, why = app:command('status', function(args, meta)

        app:log('info', 'Status requested with '..#args..' arguments.')

    end)

    if command then

        app:log('info', 'Console command: '..command.name)

    else app:log('warn', why.message) end

end)

app:keybind{key, modifiers?, callback} accepts integer key codes 1–255 and up to three distinct modifiers: Shift 16, Control 17 and Alt 18. Modifiers must form a dense list. Their order is normalized for the cooperative claim. The callback receives {key, modifiers} on the game thread. Repeated input while one callback is pending coalesces into one execution.

app:command(name, callback) registers wps.<lowercase ModID>.<name> and returns that full name in the handle. The local name starts with a lowercase letter and uses lowercase letters, digits, _ or -, up to 48 characters. Its callback receives a copied argument array and {name, command}; the console's temporary output device is never retained. There are at most 32 string arguments, 512 bytes each and 4 KiB combined; the full command is also at most 4 KiB. Invalid callback inputs are refused. The game must have a usable console for a player to enter the command.

Both helpers return handle:status() with active/name/key/modifiers/pending fields as applicable and game-thread handle:cancel(). A context may create 32 distinct runtime registrations over its lifetime and queue 16 total input callbacks. Cancelling and reusing an existing name/chord reuses its runtime registration; it does not consume another registration slot. Errors include invalid_keybind, invalid_command, unsupported, duplicate_binding, binding_limit, registration_failed, input_queue_limit, conflict, wrong_thread and standard context errors.

Cancellation is logical because the pinned Lua API does not expose unregister functions for these registrations. An inactive callback remains inert until mod unload; closing a context cancels pending calls and releases its cooperative claims. Replaced-context callbacks check readiness before author code can run. Claims prevent two Framework consumers from owning the same canonical chord or command. They do not reserve keys against independent mods or the game's own controls; different modifier combinations may still overlap according to the runtime's input matching rules.

Cleanup and verification scope

Call app:close() on the game thread while the mod and its targets are still loaded. Close withdraws cross-mod providers, cancels subscriptions and request waits, cancels scheduled/input callbacks, restores tracked transactions and then removes owned widgets through the engine adapter. A failed restoration keeps those widgets available for a later cleanup attempt. Close also requests asynchronous merchant/backpack service cleanup. A successful close does not itself confirm that those game services have completed their later restoration; inspect their status before ending the runtime when that proof matters.

A failed restoration leaves the context open with cleanup_incomplete so it can report the retained claim and retry cleanup. Do not unload the callback state merely because a local handle was cancelled. Automatic process termination, forced hot reload and destroyed Unreal targets do not provide a guaranteed restoration opportunity.

The infrastructure regression suite executes independent Lua 5.4 states connected only through shared strings. It covers multi-client discovery and replies, malformed input, overflow, timeouts, replay, replacement, callback failures, disk conflicts, schema migration, scheduling, key/command cleanup and scalar rollback. These checks prove the framework's local contracts. Each new game property, native bookkeeping callback, UI target or mod-specific service still needs acceptance against the installed game build; the helper cannot infer that an arbitrary property assignment is a complete native operation.

Copyright © 2026 Wasted Potential Studios. All rights reserved. See the package's COPYRIGHT.txt for author permissions.

<!-- Copyright (c) 2026 Wasted Potential Studios. All rights reserved. -->

Game API reference

The Framework supplies copied game data, spatial queries, item checks and filtered events for Lua mod authors. Use these alongside the shared merchant and backpack services. Each game query requires the game thread, the reflected session interface, its own verified signatures or property shapes, and the current local Singleplayer character. Inspect app:compatibility() for the installation identity and current capability checks; matching metadata alone does not establish query results or gameplay behavior.

Framework = require('../../WPS_Framework/Scripts/api')
local app = Framework.new { id='ExampleSurvey', version='1.0.0' }

app:defer(1000, function()
    local result, problem = app:world_query('npcs', {
        radius_cm=15000,
        limit=16,
        xy=true
    })
    if not result then
        app:status(problem.code, problem.message)
        return
    end
    app:log('info', tostring(result.total)..' nearby NPCs reported.')
end)

Return values and lifetime

Successful queries return a table with ok=true and build. Failed queries return nil, {code, message}. Snapshots contain ordinary Lua values and small object descriptions (available, name, class), never live UObject handles. Modifying a returned table does not change the game. The Framework resolves the current session on every query and discards results if the character or world changes during that call.

player_snapshot, vehicle_snapshot and equipment_snapshot retain individually available results and report missing pieces in unavailable. Component snapshots report unsupported fields in omitted. A successful partial snapshot does not imply that all fields were available. The API does not silently substitute zero for missing game data.

Complete method list

Method Result and purpose
app:game_capabilities() Profile, method names, query kinds, component groups, event names and validation status. Does not call the game.
app:player_snapshot() Player description, position, spice state, damage status, scoped state, revival/ability state and the game's health, stamina, shield and energy thresholds.
app:vitals_snapshot(group, options) Current and base values of the actual reflected gameplay attributes in the selected group. Defaults to the six principal character health, stamina and power values.
app:training_snapshot(options) Training attributes, including crafting, harvesting, mining, vehicle, survival, recycling and progression bonuses.
app:component_snapshot(group, options) Reflected scalar and bounded struct fields from one supported game component.
app:game_components() Availability and actual class of each supported component on the current character/controller.
app:game_state(options) Loaded Dune game-state fields, including available Coriolis state and Singleplayer settings indicators.
app:game_settings_snapshot(options) Loaded user server-setting values such as gathering, crafting, durability, survival and world-event settings. Reads the settings object; other gameplay modifiers may also apply.
app:vehicle_snapshot() Recovered, migrated and relocation record counts, backup target description and available travel-cargo component snapshots.
app:equipment_snapshot() Selected ability, scoped/active state, power-pack identifier and shortcut component snapshots.
app:input_snapshot() Sorted copies of the controller's active action and axis names. Does not remap or inject input.
app:weather_snapshot() Native clock value, sky/aurora fields, Coriolis cycle, cold-zone information and hydration/exposure attributes.
app:crafting_snapshot(options) Current crafting process and a bounded page of known or default recipe records.
app:knowledge_snapshot() Available knowledge points, the next tech-tree index and cached intel spending.
app:world_query(kind, options) Nearby actors, hostile presence, loaded-location checks, nearest actor, hazard checks or a terrain surface query.
app:item_lookup(id, options) Find a row in loaded base-item tables and copy its supported fields; optionally select an exact table path.
app:item_requirements(id) The game's HasRequiredItem predicate for the current character. Does not count, grant or consume items.
app:on_game(event, callback) A cancellable observational event subscription filtered to the current local character/controller.

Player and attributes

player_snapshot().state provides these named getter results:

spice_vision, pvp_zone, spice_vision_status, spice_state, spice_addiction_status, self_revive_progress, selected_ability, respec_spice_cost, damage_status, scoped, can_pay_self_revive, ability_active_or_cooling, current_health_threshold, low_health_threshold, very_low_health_threshold, critical_health_threshold, low_stamina_threshold, low_shield_threshold, and low_energy_threshold.

It also reports the available reflected state fields m_bIsHydrationEnabled, m_bHeatstroke, m_bShowHelmet, m_bHasDownedStateInvulnerability, m_TotalHealingRemaining, m_TotalRemainingSpiceAmount and m_ShelterState. Enum values remain the game's integers. Health thresholds are labelled as thresholds; use the attribute snapshot for actual attribute values.

Controller getters supply unstuck_wait_seconds, respawn_wait_pve_seconds and respawn_wait_pvp_seconds (the two explicit values of the game’s PvE-zone argument). targets.sandworm and targets.key_npc contain descriptions of the game's current targets when available.

Attribute groups are character, player, hydration, spice, cold, training, shield and mitigation. The character group contains health, stamina, power, combat, healing, movement and resistance attributes. Its default selection is CurrentHealth, MaxHealth, CurrentStamina, MaxStamina, CurrentPower and MaxPower; select other exact fields explicitly. The player group covers player-specific cooldown and ability bonuses. shield covers activation and mitigation parameters; mitigation covers armour and damage-type resistance. Results use the game's field names and contain {base, current}. Only reflected structures containing verified float BaseValue and CurrentValue members are accepted. The training class contains 65 known attributes on the supported build, including CraftingSpeedMultiplier, CraftingItemMaxDurabilityMultiplier, MiningOutputBaseMultiplier, PickupYieldMultiplier, HydrationMaxMultiplier, VehicleGroundSpeedMultiplier, VehicleFlyingSpeedMultiplier, FlyingVehiclesFuelUsageMultiplier, RecycleYieldMultiplier, SurveyTimeMultiplier and LandsraadTaskContributionMultiplier.

local values, problem = app:training_snapshot {
    fields={'CraftingSpeedMultiplier', 'VehicleFlyingSpeedMultiplier'}
}
if values then
    local crafting = values.attributes.CraftingSpeedMultiplier
    app:log('info', 'Current crafting multiplier: '..tostring(crafting.current))
end

Component groups

These names resolve actual reflected component properties on DunePlayerCharacter or DunePlayerController:

Area Groups
Crafting and knowledge crafting, recipes, knowledge
Construction building, building_blueprints, building_backup
Equipment and storage equipment_shortcuts, emote_shortcuts, bank, inbox, delivery
Vehicles vehicle_cargo, harnessed_vehicle_cargo
Character and interaction radiation, cold, interactions
Progression and activities contracts, specialization, surveying, journey, story, faction, landsraad, progression

Snapshots use the component's current reflected schema. They validate the owner property and the actual object against its profiled class. The reflected declared class is checked as well when that optional runtime accessor is available. A component may be unavailable in a menu, during travel or when its feature is inactive. This inspection API does not assert that crafting queues, recipe creation, construction placement or vehicle spawning are exposed as supported mutation services.

Pass options.fields to select up to 64 exact reflected field names. Without a selection, at most 128 fields are accepted. Scalar fields and structs up to three levels are copied. Object-valued members become descriptions. Arrays, maps, sets, delegates and unsupported types are omitted, with their names recorded in omitted. Use explicit game queries for supported collection results.

The same field-selection format applies to game_state and game_settings_snapshot. An unknown field or misspelled option produces an error.

World and terrain queries

Coordinates are {x, y, z} in Unreal centimetres. Omitting location uses the current character's location. Native vector parameters must expose three double components named X, Y and Z.

Kind Options Result
players location, radius_cm, limit, xy actors, total, truncated
npcs location, radius_cm, limit, xy actors, total, truncated
hostiles location, radius_cm, xy Boolean present
location_loaded location Boolean loaded
nearest location, class_path actor description or available=false
hazard location, class_path Boolean inside for the supplied loaded hazard class
terrain_snap location, rotation={pitch,yaw,roll} Surface location and rotation; no actor is moved

Radius defaults to 5,000 cm and is limited to 300,000 cm. Returned actor descriptions default to 32 and are limited to 128; an engine result above 4,096 entries is rejected. xy=true requests the native two-dimensional area check. class_path must identify a loaded native class such as /Script/DuneSandbox.DunePlayerCharacter.

nearest invokes the game's closest-actor query, which has no radius parameter. Call it when needed rather than every frame. Output limits do not change the work performed inside the game's native query.

terrain_snap calls TrySnapToSurfaceFromLocation. Its reflected signature has no success boolean, so the returned position is not presented as a guaranteed successful surface hit. This API does not modify terrain geometry. Use weather_snapshot for the available clock and environment state. These read-only adapters do not change weather or terrain.

Weather and sky state

weather_snapshot() reports native_time_of_day from TimeOfDayFunctionLibrary.GetTimeOfDay. The game's native numeric units are preserved; the API does not label this value as hours or convert it to a clock without verified semantics.

time_of_day.fields includes the currently available m_TimeOfDayServer, m_TimeOfDaySpeed, m_bTimeOfDayEnabled, m_bShowAurora, m_AuroraData and m_AuroraProbability fields. coriolis copies the cycle's supported seed, index and farm-time flag. Opaque DateTime internals are not interpreted. cold_zone includes the current cold biome and the game's most recently published cold debug sample. exposure returns current sun exposure, heat exhaustion, hydration and maximum hydration. Each unavailable section is recorded separately.

Crafting and knowledge

local page, problem = app:crafting_snapshot {
    source='known', -- or 'default'
    offset=0,
    limit=32
}
if page then
    app:log('info', tostring(page.total)..' recipes in this source.')
end
local knowledge = app:knowledge_snapshot()
if knowledge then
    app:log('info', tostring(knowledge.points)..' knowledge points available.')
end

Recipe pages return recipes, total, offset, source and truncated. Offset is zero-based and limited to 4,096; page size defaults to 32 and is limited to 128. The actual recipe count must remain within 4,096. Each copied RecipeInfo record contains its supported reflected fields, including the recipe identifier, quality, new flag and limited-use information. These are learned/default recipes, not a promise that a recipe is presently craftable.

The optional process snapshot reports m_CurrentlyCraftingItemData, m_CachedCraftingProcessState and the outcome inventory description. Native crafting times remain in their reported fields. Knowledge points come from the game's GetKnowledgePoints getter; the returned fields also report the next tree upgrade index and cached intel spending when available. These APIs do not spend points, learn recipes, edit queues or produce items.

Item lookup

Identifiers accept 1 to 128 letters, digits, underscores or hyphens. The Framework verifies the reflected identifier structure has exactly one Name member of type NameProperty before constructing it.

local item, problem = app:item_lookup(
    'OrnithopterLightLocomotion_Unique_Speed_4_Schematic',
    {table_path='/Game/Dune/Systems/Items/BaseItems/DT_BaseItems_Schematics.DT_BaseItems_Schematics'}
)
if item and item.found then
    -- Read the supported scalar/struct fields in item.record.
end
local requirement = app:item_requirements('OrnithopterLightLocomotion_Unique_Speed_4_Schematic')
if requirement then
    app:log('info', 'Requirement predicate: '..tostring(requirement.present))
end

The record is read directly with the installed runtime's UDataTable.FindRow helper and copied from a loaded table whose row type is exactly BaseItemTableRow. Omit options.table_path to search at most 128 matching loaded tables, from a discovery list limited to 4,096 tables. Duplicate identifiers return ambiguous_item; select the exact table path to resolve them. This lookup does not reproduce game-specific composite-table precedence or load assets. The game’s large FindBaseItemRecord UFunction is blocked: its 762-byte argument layout exceeds the pinned runtime’s 512-byte call buffer. The requirement check uses the small DuneGameplayUtilities.HasRequiredItem signature. These calls do not broaden the merchant service's supported catalog or guarantee that an item can be purchased, crafted or equipped.

Events

Supported names are ability_activated, ability_ended, ability_committed, item_durability, item_max_durability, item_pickup, item_spent, schematic_learned, xp_gained, auto_drink, cold_biome, spice_state, shelter_state, sandworm_targeted, sandworm_untargeted and respawn_processed.

local subscription, problem = app:on_game('item_durability', function(event)
    local value = event.parameters.NewPercentage
    if value then app:log('info', 'Item durability: '..tostring(value)) end
end)
-- Later, from the game thread:
if subscription then subscription:cancel() end

Framework 1.4.6 also provides four local component notifications. These activate only when subscribed to and run after the native handler returns:

Event Checked local component Meaning
tech_points_changed Knowledge The tech-knowledge points notification handler ran.
recipes_changed Recipes The known-recipe list notification handler ran.
crafting_item_changed Crafting The currently crafting item notification handler ran.
contracts_changed Contracts The active-contract list notification handler ran.

These handlers have no reflected arguments. Their payload includes phase='post' and an empty parameters object. Use a supported snapshot when you need current values. Each subscription checks its exact native signature and filters to the current local component, including after component replacement. A notification does not guarantee that a player action succeeded or cover changes made through other native paths.

Use Framework 1.4.6 or newer for these four names. Register the subscription during normal mod initialization, then cancel it when the observing feature is disabled. Post registration uses the pinned runtime's native pre/post hook pair; the pre callback is inert and the observer runs in the post callback. This option is for native UFunctions and is not a generic post hook for script functions. No subscription means no event hook for that feature.

Payloads contain event, subject, copied parameters and omitted. Parameter names retain the game's reflected names. For example, durability exposes Index, ItemEntity, OldPercentage and NewPercentage when those values can be copied. subscription:stats() returns delivered, ignored and invalid payload counts.

The original sixteen character/controller events observe the function before its original execution; the four component notifications above observe it after execution. An event is a notification that the function was invoked, not proof that its eventual gameplay operation succeeded. Callback return values never replace the game's return value. Events from other characters, missing local sessions and non-game threads are ignored. The filter resolves the current character on each callback, so it follows normal character replacement without retaining an old pawn.

At most 32 event handles are retained per mod context. Cancel handles when they are no longer needed. Framework context cleanup cancels the underlying hooks. Event registration and all callbacks obey the supported build and Singleplayer gates.

Validation and source evidence

The four Framework 1.4.6 post-notification definitions were checked against the retained Steam 25611704 interfaces. Isolated source and bundled fixtures cover post registration, local-component filtering, replacement, cancellation and rejection boundaries. Their delivery in a loaded current-game session remains unverified. The historical query acceptance below retains its Steam 25486029 provenance; it does not become current-build acceptance because an unchanged signature passes.

This expansion has 60 isolated boundary and marshalling tests. All 56 gameplay function signatures matched the loaded Steam 25486029 runtime. A completed read-only acceptance run in a loaded local Singleplayer character state exercised 49 game queries: 21 returned with no omissions reported, 23 returned with omitted fields, one returned partial data and four reported unavailable components. All six checked gameplay and engine source files matched the deployed files. Five additional report tests check nested coverage and exclusion of captured character values.

The 23 results with omissions include component, equipment, weather and item snapshots. The copier excluded unsupported container, delegate or reference fields and reported them in omitted; those fields are not covered by the successful return. The vehicle snapshot returned partial data because travel cargo components were unavailable. Story, progression and both travel cargo component queries were also unavailable in that character state. An object description with available=false, such as an absent current target, is a valid availability result rather than a failed query.

The direct item-table lookup found the selected schematic row and copied its supported fields. That live check used one explicit loaded table; automatic table discovery and duplicate-ID resolution remain covered by isolated tests. Recipe coverage used the first known-recipe page. Nearby player and NPC queries returned empty lists, so populated-list handling remains covered by isolated tests. The nearest-actor and hazard query variants, event delivery and unexercised field combinations do not acquire live acceptance from this run.

The Kit catalog records each operation's exact status, exercised parameters and nested omission counts. A successful result covers those requested values in the captured state, not every field or gameplay condition. The broad game_capabilities() metadata remains validation='live_reflection_and_offline_contracts', reflection_verified=true and live_verified=false: that aggregate flag does not claim completion for the entire API surface. Consult the per-operation catalog and the verification table for the completed live checks.

The Dune interface profiles come from the locally captured Steam 25486029 reflected classes. They contain function names, parameter types and property names, with no executable game code or native function addresses. Reflected parameter positions are used only to reject a changed call layout. Each function call checks the native flag, parameter order, parameter count, types, parameter byte positions and named structure classes. Item identifiers, vectors, rotations and array element types receive additional shape checks. Native calls also require a verified parameter footprint within the pinned runtime’s 512-byte argument buffer; oversized or unknown footprints fail closed. Character and world identity are checked before returning results.

The Framework invokes the bound UFunction obtained from its checked target after verifying the reflected function identity. The pinned UE4SS source supplies reflected property iteration, local struct-table conversion, out-parameter table updates and observational hook callbacks. Collection pages use bounded direct array indexing. The three engine query signatures are also checked against installed Unreal Engine 5.2.1 headers: Actor.h for actor location/rotation and GameplayStatics.h for game state. Property flags are not exposed by the pinned Lua FProperty interface. The pinned Bool/Object wrappers also share a Lua metatable name, so the optional GetPropertyClass accessor may be absent depending on initialization order. The Framework checks the actual reflected property kind and independently validates returned component classes; it does not rely on the wrapper's type() label.

Native reflected signatures, mocked execution and live gameplay are separate evidence. The independent author probe audits the gameplay and engine signature catalogs at the menu, then writes bounded read-only query results when a local character is ready. The complete operation catalog is available in the Modder Kit through wps.py api --source game and the API browser. Raw probe values remain local; the public verification summary contains structural outcomes and source hashes, with no captured character values or object identities. Game exports are not part of the public Modder Kit.

Copyright (c) 2026 Wasted Potential Studios. All rights reserved.

Unreal engine services

The engine adapters support UI styling, existing mesh and material changes, morph targets, animation, physics, lighting, fog, landscape materials, cameras and audio. Use the same ownership, readback and restoration rules across those domains. The optional Kit's API lookup lists each operation and its required object class.

Engine entry points

API Use
app:engine_api() Return a copied catalog of all named query/change operations, argument types, requirements and verification notes. Does no game lookup.
app:objects{class, limit=64, contains=''} Find currently loaded instances of one reflected class. Returns local Unreal object handles, excluding class defaults. The result limit is 1–256.
app:asset(path, class) Resolve an already loaded asset and verify its class. Returns its local Unreal object handle.
app:engine_query(key, objectOrResolver, args) Read a supported operation as a plain scalar, vector, color or asset full-name string. Returns value, or nil, problem.
app:engine_change(key, objectOrResolver, value, args) Apply and verify a supported change under a canonical shared resource claim. Returns a transaction with :rollback(), or nil, problem.
app:engine_action(key, object, args) Invoke one of the animation, physics or audio actions listed below. Returns true, or nil, problem. Actions have effects over time and do not return a restoration transaction.
app:ui_create(widgetClassPath, {z_order=0}) Create and add a loaded UserWidget class to the local player's viewport. Returns an owned widget handle with widget, active and :close().

Except for engine_api, these calls run on the game thread after the reflected-session and Singleplayer checks. Each operation also checks its own native signatures, arguments and property shapes. Use app:defer, app:every or a suitable Framework callback. Query results can become stale; they do not authorize a later write. An object resolver is called again for every transaction read and write. If it returns a different object address, restoration is refused instead of touching the replacement.

objects calls UE4SS's class search only when requested. Its result is bounded, but the underlying engine search can examine every loaded instance of that class. Avoid broad per-frame discovery. Discover specific classes at a suitable lifecycle event, then retain a resolver that checks the current target.

A managed UI change

This example locates a text widget whose loaded name contains the author's known widget name. It changes only that selected widget's opacity and restores it five seconds later. Replace the filter with the name of a widget you own; an empty result is normal when that UI is closed.

app:defer(1000, function()
    local widgets, problem = app:objects {
        class = '/Script/UMG.TextBlock',
        contains = 'MyModStatus',
        limit = 1
    }
    if not widgets or not widgets[1] then return end
    local tx, problem = app:engine_change('ui.opacity', widgets[1], 0.75)
    if not tx then app:log('warn', problem.message); return end
    app:defer(5000, function()
        local restored, problem = tx:rollback()
        if not restored then app:log('warn', problem.message) end
    end)
end)

Values and ownership

Pass named operation arguments in the final table, for example {slot=2} for a material slot or {name='Tint'} for a material parameter. Unknown arguments are rejected. Slot indices start at zero. Vectors use {X, Y, Z}, 2D vectors use {X, Y}, rotations use {Pitch, Yaw, Roll}, and linear colors use {R, G, B, A}. These names are keys: {X=1,Y=2,Z=3} is a vector. All components must be finite numbers.

Asset queries return an existing asset's full-name string. Pass that string or its object path to app:asset with the required class to obtain a local handle. Asset replacement requires both the old and new assets to be valid and loaded; a missing original asset cannot provide a restorable transaction. The framework does not invent asset identifiers or silently load external packages.

Managed changes use a resource identity based on the target object, setter, slot and parameter. Two participating contexts therefore conflict even if the authors give their mods different names. Readback accepts a small floating-point tolerance. A changed value written by the game or another mod blocks rollback. Failed writes attempt reverse restoration; inspect problem.transaction and its recovery state if restoration fails. The Framework cannot restore a destroyed object or make an incompatible asset safe.

Changing a text or progress widget that has a binding can remove that binding. Use the text/progress adapters on unbound widgets your mod owns. Text restoration restores displayed text, not its previous localization namespace, history or binding. Skeletal mesh replacement requires the same loaded skeleton, but changing back does not restore the old pose or animation-instance state. animation.rate changes require single-node animation mode.

Create a mod widget

ui_create verifies that the loaded class derives from UMG.UserWidget, uses the current local player controller, and permits at most 32 active widgets per mod context. z_order is an integer from -100 to 10000. The returned handle.widget is a local Unreal object and must never be sent through cross-mod messages or written to a data store. Call handle:close() on the game thread to remove it from the viewport. Closing the app also removes its owned widgets; repeated removal is harmless. Widget removal does not forcibly destroy an object retained elsewhere by the game.

The widget class must already be available to the current game build. This API does not load an external container or change Dune's container validation. A native author can also use the included SDK/project helpers to implement UI that does not depend on a new cooked widget package. See the author guide for the checked Pak-format requirements and the separate historical and current signing evidence.

Animation, physics and audio actions

Action key Target class Arguments Effect
animation.play Engine.SkeletalMeshComponent {asset='loaded animation path', looping=false} Play an existing compatible animation asset using single-node playback. This can replace Animation Blueprint control.
animation.stop Engine.SkeletalMeshComponent {} Stop single-node playback. Does not restore a previous animation graph or pose.
physics.impulse Engine.PrimitiveComponent {vector={X=0,Y=0,Z=100}, bone='None', velocity_change=false} Apply one impulse; each component is bounded to ±10000000. The result is not reversible.
physics.force Engine.PrimitiveComponent Same arguments Apply a force for this call. Here velocity_change=true selects acceleration-change semantics independent of mass. Repeated application requires an explicitly scheduled author callback.
audio.play Engine.AudioComponent {start=0} Start the component's assigned sound at the given seconds, bounded to 0–86400.
audio.stop Engine.AudioComponent {} Stop the component's assigned sound.

Animation compatibility, authority, a valid physics body and an assigned sound remain engine requirements. A successful call confirms invocation, not that the requested effect was visible or audible. These actions do not provide a synthetic undo for elapsed playback, movement or collisions.

Weather and terrain scope

Fog and lighting adapters alter existing engine components. The Dune weather system may subsequently overwrite those values; use status/readback and avoid continuously fighting the game. These controls do not themselves schedule storms, change water or heat simulation, or set the game's day/night clock. Those require separately verified game services.

terrain.dynamic_material resolves a landscape component's dynamic material only where the landscape was configured to use dynamic material instances. Apply the appearance.scalar, appearance.color or appearance.texture services to that returned material for existing terrain shader parameters. This supports terrain appearance changes. Runtime heightmap sculpting, new landscape topology, collision rebuilding and terrain streaming changes are not provided. The author tools prepare and inspect cooked content. For Steam 25611704, the Kit verifies the checked custom Pak layout, shipped game signatures and container payload hashes. Current native inspection confirms IoStore signature enforcement. Its signing builder requires an authorized private key matching the game’s public key; no matching game private key was found in the examined WPS configuration. These format and signature checks do not establish authored-asset loading or gameplay acceptance. The author guide gives the exact coverage.

Engine verification and limitations

The operation definitions were checked against the installed Unreal Engine 5.2.1 headers and captured game reflection data. Offline tests cover scalar/struct marshalling contracts, ownership conflicts, readback, rollback, invalid values, replaced targets, missing assets and widget cleanup. Each native call checks the reflected session route, native function flag, parameter order, names, types, offsets, struct layout and parameter-buffer capacity before invocation. Field-name casing follows the running engine, including linear colors. Name parameters are converted to native FName values automatically. These checks do not replace gameplay acceptance for each PC edition, target, asset and effect. Consult the release's verification record for live observations; unexercised operations are experimental integrations even when their engine declarations are present.

No hooks, timers or class scans are started just by importing the engine module. Native Blueprint callbacks, arbitrary memory offsets, new content signing keys and online-game operation are outside this API. The UE4SS FText documentation and UStruct reflection documentation describe the runtime mechanisms used here.

Named engine operations

The following table is generated from the same catalog included in the Framework and Modder Kit. Query means engine_query; Change means engine_change returns a managed transaction. A value's practical effect still depends on the loaded game object.

Animation engine operations

Key Object class Value Arguments Access Notes
animation.mode /Script/Engine.SkeletalMeshComponent integer 0 to 2 None Query
animation.playing /Script/Engine.SkeletalMeshComponent boolean None Query
animation.position /Script/Engine.SkeletalMeshComponent number 0 to 86400 None Query Read-only timeline snapshot; a moving timeline is not suitable for transactional restoration.
animation.rate /Script/Engine.SkeletalMeshComponent number -100 to 100 None Query + Change Single-node animation playback only. Animation Blueprint graphs control their own rates.

Appearance engine operations

Key Object class Value Arguments Access Notes
appearance.color /Script/Engine.MaterialInstanceDynamic color name Query + Change Requires an existing vector parameter in a dynamic material instance.
appearance.material /Script/Engine.PrimitiveComponent asset:/Script/Engine.MaterialInterface slot=0 Query + Change
appearance.morph /Script/Engine.SkeletalMeshComponent number -10 to 10 name Query + Change Animation may overwrite morph weights; unexpected changes prevent rollback.
appearance.scalar /Script/Engine.MaterialInstanceDynamic number -1000000000.0 to 1000000000.0 name Query + Change Requires an existing scalar parameter in a dynamic material instance.
appearance.skeletal_mesh /Script/Engine.SkeletalMeshComponent asset:/Script/Engine.SkeletalMesh None Query + Change Existing compatible skeleton; pose and animation state are not restored by changing the asset back.
appearance.static_mesh /Script/Engine.StaticMeshComponent asset:/Script/Engine.StaticMesh None Query + Change Existing loaded mesh; collision/navigation consequences depend on the component and asset.
appearance.texture /Script/Engine.MaterialInstanceDynamic asset:/Script/Engine.Texture name Query + Change

Audio engine operations

Key Object class Value Arguments Access Notes
audio.pitch /Script/Engine.AudioComponent number 0.125 to 4 None Query + Change
audio.volume /Script/Engine.AudioComponent number 0 to 10 None Query + Change

Camera engine operations

Key Object class Value Arguments Access Notes
camera.aspect /Script/Engine.CameraComponent number 0.1 to 10 None Query + Change
camera.fov /Script/Engine.CameraComponent number 5 to 170 None Query + Change

Physics engine operations

Key Object class Value Arguments Access Notes
physics.angular_damping /Script/Engine.PrimitiveComponent number 0 to 1000000.0 None Query + Change
physics.collision /Script/Engine.PrimitiveComponent integer 0 to 5 None Query + Change Collision enum: 0 none, 1 queries, 2 physics, 3 both, 4 probes, 5 queries and probes.
physics.gravity /Script/Engine.PrimitiveComponent boolean None Query + Change
physics.linear_damping /Script/Engine.PrimitiveComponent number 0 to 1000000.0 None Query + Change
physics.mass /Script/Engine.PrimitiveComponent number 0 to 1000000000000.0 None Query
physics.simulating /Script/Engine.PrimitiveComponent boolean bone=None Query
physics.velocity /Script/Engine.PrimitiveComponent vec3 bone=None Query

Scene engine operations

Key Object class Value Arguments Access Notes
scene.hidden /Script/Engine.SceneComponent boolean None Query + Change
scene.location /Script/Engine.SceneComponent vec3 None Query
scene.rotation /Script/Engine.SceneComponent rotator None Query
scene.scale /Script/Engine.SceneComponent vec3 None Query + Change Relative component scale; child components inherit transforms.
scene.visible /Script/Engine.SceneComponent boolean None Query + Change
scene.world_scale /Script/Engine.SceneComponent vec3 None Query

Terrain engine operations

Key Object class Value Arguments Access Notes
terrain.dynamic_material /Script/Landscape.LandscapeComponent asset:/Script/Engine.MaterialInstanceDynamic slot=0 Query Available only where the landscape was configured to use dynamic material instances. Use appearance.scalar/color/texture on the returned loaded asset.

Ui engine operations

Key Object class Value Arguments Access Notes
ui.angle /Script/UMG.Widget number -36000 to 36000 None Query + Change
ui.enabled /Script/UMG.Widget boolean None Query + Change
ui.opacity /Script/UMG.Widget number 0 to 1 None Query + Change
ui.pivot /Script/UMG.Widget vec2 None Query + Change
ui.progress /Script/UMG.ProgressBar number 0 to 1 None Query + Change Use an unbound progress bar owned by your UI.
ui.scale /Script/UMG.Widget vec2 None Query + Change
ui.text /Script/UMG.TextBlock text None Query + Change Replacing a bound text value can remove its binding; use only author-owned unbound text widgets.
ui.translation /Script/UMG.Widget vec2 None Query + Change
ui.visibility /Script/UMG.Widget integer 0 to 4 None Query + Change Slate visibility enum; 0 Visible, 1 Collapsed, 2 Hidden, 3 HitTestInvisible, 4 SelfHitTestInvisible.

Weather engine operations

Key Object class Value Arguments Access Notes
weather.fog_color /Script/Engine.ExponentialHeightFogComponent color None Query + Change
weather.fog_cutoff /Script/Engine.ExponentialHeightFogComponent number 0 to 1000000000.0 None Query + Change
weather.fog_density /Script/Engine.ExponentialHeightFogComponent number 0 to 10 None Query + Change
weather.fog_falloff /Script/Engine.ExponentialHeightFogComponent number 0 to 100 None Query + Change
weather.fog_opacity /Script/Engine.ExponentialHeightFogComponent number 0 to 1 None Query + Change
weather.fog_start /Script/Engine.ExponentialHeightFogComponent number 0 to 1000000000.0 None Query + Change
weather.light_intensity /Script/Engine.LightComponent number 0 to 1000000000.0 None Query + Change
weather.light_temperature /Script/Engine.LightComponent number 1000 to 15000 None Query + Change
weather.sky_intensity /Script/Engine.SkyLightComponent number 0 to 1000000000.0 None Query + Change

Compatibility and release verification

This Framework 1.7.1 uses UE4SS 3.0.1-1140-gf58e8f84 through the Launcher 1.2.13. The Inventory-layout catalogue retains Steam builds 25486029, 25611704, 25690505 and 25789117, and Microsoft PC packages 2.0.7.0 through 2.0.12.0. Stacks, Economy, Base, Water and creation retain their own operation-specific code and interface checks; an Inventory profile does not expand their coverage. The Xbox app here means Windows PC.

The Framework 1.7.1 and Launcher 2.1.1 retain the pinned UE4SS 1140 runtime. The Launcher enables all 14 upstream hook settings with Dune virtual-table slots ProcessEvent 85, ProcessConsoleExec 88 and LoadMap 169. Native source, binary identities, isolated fixtures, package checks and loaded-game observations establish separate scopes of evidence.

Launcher 1.2.13 passed eight standalone main-menu checks on Steam build 25690505: actual Blueprint and native callback delivery, preserved parameters and return values, and callback removal after unregistration. Session recovery completed and all 102 monitored save files retained their hashes. Microsoft PC 2.0.10.0 has static mapping evidence only. This test does not establish every enabled hook's callback behavior, Fast Build gameplay, world transitions, transactions, save/reload or performance. UStruct::Link is an internal facility and was not exercised as a mod callback.

The retained Steam 25690505 executable and d02 decompilation project share SHA-256 dafb653e42099023b99980e00311d86529fef78584acb18a1f5070d309826b5b. Complete targeted decompilation of five function bodies corroborated the two corrected targets and the unrelated helpers selected by the old mappings. The console wrapper calls the command parser; the corrected LoadMap accepts the world context and URL. Microsoft static evidence comes from the retained 2.0.10.0 loaded-code image and does not replace an edition-specific runtime test.

Independent evidence

Retained patch investigation

The Steam 25611704 investigation compared a captured installation with the retained Steam 25486029 baseline. Its selected patch work finished with explicit partial limits:

Investigation Recorded outcome
Selected native functions 127 processed: 20 recovered, 107 partial.
Native module comparison One module with changed code; 79 reused modules.
Asset-export comparisons 157,158 scheduled: 156,074 unchanged exports, 973 changed and 111 added.
Captured container inputs 44.

These are investigation coverage counts, not a claim that the entire game was fully decompiled or that every API works. Reused baseline results keep their original provenance. A partial function result does not authorize a native call; active Framework capabilities still require their own complete interface, code and lifetime checks. Raw game exports, executable content, addresses and captured player data are not distributed in the public Kit or these guides.

The retained Steam 25611704 container investigation independently checked Dune's custom Pak reader against the installed executable and two stock Pak footers. A standard version-11 Pak's 221-byte footer does not satisfy Dune's checked 261-byte layout. The supplied texture Pak was incompatible with that reader. That native inspection confirmed IoStore signature enforcement. Independent verification authenticated both signatures and all 9,169 stored data blocks in a shipped IoStore container, plus a shipped Pak's signature and CRC blocks. The Kit signing builder passed a separate test-key roundtrip and wrong-key rejection checks; it requires an authorized private key matching the game's public key, which was not found in the examined WPS configuration. These checks establish the recorded formats and verification behavior, not game acceptance of arbitrary authored content. The author guide describes the available tools and their exact limits.

The Framework 1.4.6 record covered four local post notifications with checked interfaces and isolated source/bundled lifecycle tests. That offline record alone does not establish loaded-game event delivery. On-demand service construction, cross-state wake, stopped cleanup, idle sleep and retained failure recovery are covered by isolated lifecycle tests; these do not establish a live FPS improvement or merchant transaction acceptance.

Operation and gameplay records

Reflected operations validate their required signatures and property shapes. Private layouts and native setters additionally require their own checked profiles. An unavailable capability reports its reason; it must not disable unrelated reflected operations or services. Captured metadata from older builds retains its original provenance and is not automatically evidence for the current candidate.

Offline tests cover scalar transport between separate Lua states, ownership, revisions, malformed requests, cleanup, schedules, catalog and price rules, capacity calculations, occupied floors, migrations and package contents. Framework source and the bundled distribution must both pass applicable service fixtures. Native code compilation and signature checks are separate from runtime execution.

The current Steam combined Inventory and Stacks Up test verified eight chests and two vehicle cargo inventories without errors. Backpack limits remained at 170 slots and 3,500 volume across actual world-map travel and relog, preserving 104 saved item identities, types and quantities. This save-specific occupied floor is not a new default capacity. Individual transfers and Xbox loaded-world gameplay remain separate acceptance gates.

Stacks Up 1.0.3 requires Framework 1.4.5. Generic stack-service fixtures cover same-preset revalidation, late inventory readiness, retained cleanup and world/table transitions. The corrected signed Framework run on Steam build 25611704 acknowledged Stack100's loaded definition and item-limit readbacks and Economy activation in the same session without the shared-hook initialization failure. The player confirmed the reported freezing was resolved. A 90-second settled-game capture recorded 9,809 frames, a 12.203 ms 99th-percentile interval, a 112.817 ms maximum interval and no intervals above 250 ms. The loading capture separately recorded a 1,250.272 ms maximum interval. These are observations of the tested session, not a controlled comparison or attribution of every stall. The interrupted inventory capture exercised no transfer or merge action. Transfer, merge, split, save/reload and other presets require their own direct records.

Live startup checks establish only what the captured session actually exercised. Storage transfers, vehicle cargo-module changes, merchant transactions and save/reload require direct acceptance records for those operations. A clean loader log or a populated shop list does not prove a purchase completed or persisted. Historical query and authoring captures retain their original build provenance.

Distribution

Player archives contain runtime files, metadata, signed catalogs and necessary notices. Framework retains the public Scripts/main.lua and Scripts/api.lua entry paths with its internal implementation bundled deterministically. The launcher preserves the pinned UE4SS 1140 binaries; its documented configuration is separate from the Framework API. The saved Economy interface binds its three checked widget functions individually; these private bindings do not require global script-dispatcher hooks. WPS executable payloads use the approved Microsoft Public Trust identity; the remaining payloads are covered by signed catalogs. The optional Modder Kit retains author tools, schemas, searchable API data and examples. Illustrated guides and media are hosted on the WPS website.

The release manifest records source inputs, candidate versions, exact package allowlists, executable ownership and hashes. Packaging verifies source freshness, signatures, timestamps, catalog membership and archive contents before Defender scans. Local signing and scanning are evidence about those exact bytes, not a general compatibility or antivirus reputation guarantee.

Public release identity is checked separately against the actual download's version and hashes. A published archive does not expand the operation coverage recorded above. New Kit guidance and updated guides likewise do not establish a previously untested game operation.

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. 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.

Capacity and merchant maintenance

Framework 1.7.1 has checked native profiles for Steam build 26083551 and Microsoft PC package 2.0.15.0. Steam Inventory and Stacks Up passed actual world-map travel and a separate relog together; all 104 saved item identities, types and quantities were retained. Xbox four-mod startup and independent live checks of 82 native ranges and three vtable bindings passed. Xbox loaded-world gameplay remains unverified. Native bodies, current object identities and reflected interfaces remain independently checked; installation labels alone never authorise private access.

Slotless contract entries do not increase occupied backpack slots. The game custom volume multiplier applies once before the WPS factor. Both chest slots and volume scale together. Skip new effective 1x requests when no owned change needs restoring.

Use app:economy for shared merchant contributions. Twelve omitted schematic resale references are included while higher native quotes and buying availability remain protected. Individual quote widgets and Buy-tab one-Solari reports remain unverified.