Skip to content

setObjectScale: add optional scaleCollision argument to match collision to visual scale (collision scale)#4985

Open
TheCrazy17 wants to merge 12 commits into
multitheftauto:masterfrom
TheCrazy17:feat/object-scale-collision
Open

setObjectScale: add optional scaleCollision argument to match collision to visual scale (collision scale)#4985
TheCrazy17 wants to merge 12 commits into
multitheftauto:masterfrom
TheCrazy17:feat/object-scale-collision

Conversation

@TheCrazy17

@TheCrazy17 TheCrazy17 commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds an optional scaleCollision boolean to setObjectScale (client and server-side). When true, the collision model is cloned and scaled to match the visual.

setObjectScale(object, scale [, scaleY, scaleZ, scaleCollision = false])

Defaults to false for full backward compatibility, so existing scripts are unaffected.

mta-screen_2026-06-26_23-06-51 mta-screen_2026-06-26_23-07-03

Motivation

Closes #3775.

Objects scaled visually kept their original collision size, causing an obvious mismatch (e.g. a giant object you can walk through). The argument is opt-in rather than automatic to avoid breaking existing scripts that rely on the old behavior, for example, some DM maps scale objects to make them non-solid intentionally.

Open to feedback and improvements on the implementation approach.

Test plan

setObjectScale(obj, 3, 4, 1, true) -> collision matches the enlarged visual (stand on/collide with it).
setObjectScale(obj, 3, 4, 1) (omitted) -> collision unchanged, backward compatible.
Scale an object the same frame it is created -> works correctly.
Disconnect and reconnect with a scaled object -> collision is restored.
Server sets scale with scaleCollision = true -> clients receive and apply it.
Existing scripts using setObjectScale without the new argument, behavior identical to before.

Checklist

  • Your code should follow the coding guidelines.
  • Smaller pull requests are easier to review. If your pull request is beefy, your pull request should be reviewable commit-by-commit.

Builds a new CColModel by rescaling a model's collision geometry
(spheres, boxes, vertices, suspension lines/disks) and feeding it
through the engine's own COL3 parser, so memory ownership ends up
identical to a normal custom .col load. Non-uniform scale gets
rejected when the source has collision spheres, disks or lines,
since those only carry a single radius and can't be scaled correctly
per axis.

Also adds CModelInfoSA::GetColModelInterface() to read a model's
current collision before scaling it.
AcquireScaledCollisionModel clones a base model into a free custom
model ID with scaled collision, sharing that clone with any other
caller asking for the same model and scale instead of duplicating it.
ReleaseScaledCollisionModel drops a reference and frees the clone's
collision and model slot once nobody's using it anymore.

Releasing detaches the collision first, same order engineReplaceCOL
already uses, then destroys it and frees the model slot. Freeing the
slot first would make CModelInfoSA::Remove skip the actual unload
while it still thinks a custom col model is assigned.
SetScale now takes a bScaleCollision flag, default false so nothing
existing changes. When it's on, the object switches to a scaled
collision clone from CClientModelManager; turning it off (or
destroying the object) switches back to the real model and releases
the clone. The real base model is kept separately so re-scaling or
disabling it later always starts from the original, not a clone.

The clone only gets released after Destroy()/SetModel() already
dropped this object's own reference to it, not before, otherwise its
model info could get freed while still in use.
setObjectScale(object, scale[, scaleY, scaleZ, scaleCollision=false]),
defaulting to false so every existing script keeps its current
visual-only scaling behaviour.
CObject now stores the scaleCollision flag alongside its scale and
passes it through setObjectScale on the server's Lua API. It's synced
to clients both on live changes (the SET_OBJECT_SCALE RPC) and on
initial entity creation, so players who join later still apply the
same collision scaling as everyone else.
CClientObject::Create() applied scale through the full
CClientObject::SetScale(), which can acquire or release a scaled
collision clone and call SetModel(). That destroys and recursively
re-creates the very object currently being constructed, and if the
resulting model needs to stream in asynchronously, m_pObject is left
null for the rest of Create(), crashing on the next access to it (for
example SetAreaCode).

By the time Create() runs, the collision clone bookkeeping is already
settled, since it's what's streaming m_usModel in, so only the visual
scale needs to be applied here. Skip CClientObject::SetScale() and call
m_pObject->SetScale() directly instead.
setObjectScale(obj, x, y, z) without the scaleCollision argument
defaulted it to false, which meant calling it again to tweak the visual
scale alone would turn off any collision scaling that was already
active, on both the client and the server.

scaleCollision is now optional end to end (Lua argument, static
function definitions, and CClientObject::SetScale). Leaving it
unspecified preserves whatever collision-scaling state the object
already has, instead of resetting it. Explicitly passing true or false
still works as before.
…llision

CClientManager's destructor deletes and nulls each manager in a fixed
order (pickups, then objects, among others). Releasing a scaled
collision clone in CClientObject's destructor can cascade into
CClientModel::RestoreDFF, which restores every entity type that could
be using the model, including pickups and buildings, with no null
checks on those manager pointers. During full client shutdown, the
pickup manager (and potentially others) is already gone by the time
the object manager destroys its objects, so this crashed.

RestoreDFF now skips each restore step whose manager isn't available
instead of assuming it always is.
Both CClientObject::Create() and SetScale() applied the visual scale
after ProcessCollision()/UpdateVisibility() had already run (or, for
SetScale(), after a model swap that re-runs Create() with the old
scale, since m_vecScale was only updated at the very end of the
function). That registers the object's collision and visibility bounds
at its old, usually unscaled, size, so a scaled object could flicker in
and out of view at some camera angles even after a single scale call.

m_vecScale is now updated before SetModel() can trigger a recreation,
and Create() applies the scale before processing collision and
visibility, so both always reflect the object's real size from the
moment it's registered.
The scaled-collision feature gives a scaled object its own cloned model
whose collision is a scaled copy of the base model's. Several issues
kept that scaled collision from actually being used:

- Root cause: CModelInfoSA::SetColModel() early-returned whenever the
  requested col model was already recorded as the custom one
  (m_pCustomColModel == pColModel). MakeCustomModel() re-invokes
  SetColModel() right after a model streams in, precisely to re-apply
  the custom collision over whatever the reload reset the interface's
  pColModel back to. The early-return turned that re-apply into a no-op,
  so a freshly streamed clone kept the original disk collision and
  ignored the scaled one. It only "fixed itself" on a second
  setObjectScale call because of leftover interface state. The guard now
  also checks the live interface still has the custom col actually
  applied, so the re-apply runs when (and only when) it's needed.

- CreateScaledColModel rejected any non-uniform scale on collisions with
  spheres/disks/lines, even though the rest of the function already
  approximates those radii with the largest scale axis (as it does for
  the bounding sphere). Removed the rejection so the approximation is
  actually used instead of silently producing no scaled collision.

- Force a blocking load of the base model's collision before cloning it,
  so scaling immediately after createObject() doesn't race the model's
  own streaming and find no collision to clone.

- Force a blocking load of the clone model before switching to it, so
  Create() runs synchronously and applies the scaled collision on the
  first call instead of waiting for an async callback that never fired.

- Clear the scaled-collision clone cache in RemoveAll(), so reconnecting
  can't hand back a cache entry whose clone no longer exists (which left
  objects visually scaled but with unscaled collision after a reconnect).
When scaling an object in the same frame it is created, its collision slot
may not have been streamed in yet, causing m_data to be nullptr. Previously,
this was incorrectly treated as a "no collision" case (visual/LOD model),
silently leaving the object unscaled.
Fix: request and force-load the collision stream slot via the engine's
streaming system before giving up. Only return nullptr if data is still
absent after the force-load, which is the genuine "no collision" case.
@Fernando-A-Rocha

Copy link
Copy Markdown
Contributor

This is in the Top 10 of most wanted MTA features in the last decade :)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Re-scale object collision automatically

2 participants