{{ message }}
Tags: Sure-Development/sure_lib
Tags
fix(spawn): drive streaming spawn/despawn from onEnter/onExit transit…
…ions
The nearby-based spawn loop introduced two real bugs:
- spawnEntityInternal yields on awaitModel (CreateThread + Citizen.Await).
The nearby tick keeps firing every 300ms while that coroutine is
suspended; each tick sees entry.spawned still false and dispatched
another CreatePed, so peds stacked every time the model finished
loading.
- The watchDistance widening pushed point.distance well past
despawnRadius. ox_lib's onExit only fires when the point falls out
of the watch window or the player crosses a grid cell, so peds
routinely stayed in the world after the player left range.
Match grand_ped-manager's simpler shape and add an idempotency guard:
- point.distance = despawnRadius or radius (single radius, no watch
hysteresis).
- onEnter -> spawn via a guarded helper that returns immediately if
entry.spawned or entry.pending is true. The pending flag is set
before spawnEntityInternal and cleared after it returns, so
concurrent onEnter / nearby ticks during model loading no-op.
- onExit -> despawn helper that no-ops when nothing is spawned.
- spawnOnNear.onNear is wired through point.nearby only when
provided, so the polling callback stays opt-in and never drives
state transitions.
Bump version 2.12.1 -> 2.12.2.
fix(spawn): despawn from nearby tick instead of relying on onExit alone ox_lib's points only fires onExit reliably when the player crosses a grid cell boundary or when the point falls out of the grid query. A player drifting in/out of despawn range inside the same cell could leave the entity spawned indefinitely, and the user reported seeing peds accumulate every time they walked back into range. Widen the lib.points distance to max(despawnRadius * 1.5, radius * 2) so the nearby tick keeps firing through the entire spawn-despawn transition, then do the radius check ourselves on every tick: - currentDistance <= radius and not spawned -> spawn - currentDistance > despawnRadius and spawned -> despawn onExit becomes the belt-and-suspenders for the case where the point truly leaves the watch window or the entire grid query. Bump version 2.12.0 -> 2.12.1.
feat(spawn): streamRadius / despawnRadius shortcut on spawn:ped|object
Move the streamRadius / despawnRadius option from slice:interact down
into sure.spawn so any caller (with or without slice) can opt into the
proximity streaming behavior without hand-rolling the spawnOnNear
table:
spawn:object('prop_rock_3_g', vector3(x, y, z), {
freeze = true,
placeOnGround = true,
streamRadius = 50,
despawnRadius = 60,
onNear = function(state) ... end,
})
spawn now derives spawnOnNear.coords from the spawn coords argument so
the proximity check defaults to the entity's location. spawnOnNear is
still accepted directly for the advanced case where the proximity
point differs from the entity coords.
slice:interact stops building spawnOnNear itself and just forwards
streamRadius / despawnRadius / its own onNear into the spawn options;
sure.spawn handles the wiring. ctx.stream / ctx.entity behavior is
unchanged.
Bump version 2.11.0 -> 2.12.0.
feat(spawn,slice): streaming spawn radius for slice:interact
Wire sure.spawn's existing spawnOnNear proximity streaming through
slice:interact so entities are only created when the player gets
within range and torn down when they leave it. Targets the typical
farming/heist pattern where a single resource can hold hundreds of
world entities without keeping them all spawned at once.
- spawn module now attaches entry:dispose() to the table returned by
registerStreamEntry so callers can tear down a single streaming
entry without reaching into module-private helpers.
- slice:interact spawn block accepts streamRadius and despawnRadius:
spawn = {
type = 'object',
streamRadius = 50,
despawnRadius = 60,
options = { freeze = true, placeOnGround = true },
}
When streamRadius is set the helper:
- shallow-clones options, fills spawnOnNear from item coords +
streamRadius/despawnRadius, and attaches an onNear that pushes
the live spawn handle into ctx.entity.
- records the stream entry on ctx.stream instead of treating the
return value as a handle.
- calls ctx.stream:dispose() on unmount, otherwise falls back to
the existing safe DeleteEntity path.
LLS stubs gain ctx.stream plus streamRadius / despawnRadius on
SureSliceInteractSpawn so authors get autocomplete and warnings.
Bump version 2.10.3 -> 2.11.0.
fix(slice): widen interact action trigger to any truthy nearby result IsControlJustReleased and other FiveM bool natives can return 1 instead of the literal true depending on the lua binding. The strict `triggered == true` gate kept action silent because `1 == true` is false in Lua. Switch to a truthy check so any truthy nearby result fires action. Bump version 2.10.2 -> 2.10.3. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
fix(slice): guard interact.spawn against non-vector3 coords / non-str… …ing model Type-check item[modelKey] and item[coordsFrom] before forwarding to sure.spawn so a stale ox_lib cache.coords value (returns false until ox_lib:cache:coords fires the first time) or a missing model field errors at the slice boundary with a useful message instead of exploding inside sure.spawn with "attempt to index a boolean value". [sure_lib][slice] interact(entities): item.coords must be a vector3 or table with x/y/z, got boolean (value: false) Bump version 2.10.1 -> 2.10.2. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
fix(slice): call slice.log via colon so message is not eaten slice was calling slice.log.error/warn with dot syntax against a logger whose methods are defined as colon methods (function logger:error(message)). That made the formatted error string become self and left message as nil, so every internal slice error printed as `[farming] [error] nil` and ate the actual failure. Switch every internal slice.log.error / slice.log.warn call to slice.log:error / slice.log:warn so the formatted message reaches the logger, matching log_test.lua's expectation and the SureLogger LLS stub. Bump version 2.10.0 -> 2.10.1. 165/165 tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
feat(slice): split interact into nearby predicate + action effect
Interact now separates the gate from the work. nearby returns a
boolean - the predicate that decides whether the interaction is
ready to fire this frame. action is a new optional callback that
runs the same frame nearby returns true, receiving the same
(slice, item, ctx) triple.
s:interact('entities', {
spawn = { type = 'object' },
nearby = function() return IsControlJustReleased(0, 38) end,
action = function(s, item)
-- play anim, wait, then s:unmount('entities', item.key)
end,
})
- nearby's old "do effects inline" pattern still works for users who
ignore action - the library only invokes action when nearby returns
true AND action is set.
- Defining action without nearby errors - action has no trigger
without a predicate.
- Debounce is on the user; ctx is per-item so a ctx.busy flag is the
natural place for "interaction in progress".
Bump version 2.9.1 -> 2.10.0. 165/165 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
fix(library): move transaction/scope back onto SureSliceInstance The transaction and scope @field lines were trailing SureSliceInteractSpec instead of SureSliceInstance, so LuaLS treated them as required members of the interact spec. Any call to s:interact(stateKey, {...}) reported "Missing required fields in type SureSliceInteractSpec: transaction, scope". Also switch every optional field on SureSliceSpec, SureSliceNetSyncConfig, SureSliceInteractContext, SureSliceInteractSpawn, and SureSliceInteractSpec from `field type?` to `field? type` so LuaLS marks the field as optional rather than only the type as nullable. Bump version 2.9.0 -> 2.9.1. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
feat(slice): interact helper that wires spawn + point + lifecycle
slice:interact(stateKey, spec) (client only) wraps slice:ref with
sure.spawn and lib.points.new so a feature can describe an entity +
interaction in one place instead of hand-wiring the four moving
parts (ref handler, sure.spawn call, lib.points.new, cleanup).
s:interact('entities', {
spawn = {
type = 'object',
modelFrom = 'model',
coordsFrom = 'coords',
options = { freeze = true, placeOnGround = true },
},
distanceFrom = 'range',
onEnter = function(s, item, ctx) lib.showTextUI('press E') end,
onExit = function(s, item, ctx) lib.hideTextUI() end,
nearby = function(s, item, ctx)
if IsControlJustReleased(0, 38) then return true end
end,
})
- spawn block resolves model/coords/heading by field name on the item
and forwards options to sure.spawn:ped / sure.spawn:object.
- point is created from lib.points.new with coords from coordsFrom
and distance from item[distanceFrom] (fallback 2.0).
- onEnter / onExit / nearby receive (slice, item, ctx). ctx.entity is
the spawned handle, ctx.point is the lib.points instance.
- nearby's return value is intentionally not interpreted - users use
return true as an internal signal and decide what to do themselves.
- cleanup on ref unmount removes the point first, then claims and
deletes the entity.
- errors on server side and when neither spawn nor a handler is set.
Prompts and UI stay outside the helper so each resource can keep its
own text-UI / target / NUI stack.
Bump version 2.8.0 -> 2.9.0. 162/162 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PreviousNext
