BLANCO

Developer · blancodagoat.dev

← All parts

FIVEM CLIENT-SIDE MODS

GTA V / RAGE file formats › Part 9

The four folders FiveM actually reads on the client, the assembly.xml package format that makes an .rpf loadable, how to ship a whole dlc.rpf as a client-side pseudo-DLC, what pure mode really checks, and the ASI plugin rules.

How to read the evidence markers on this page: no marker — verified from source or a primary spec, trust it. 🟡 — community reverse-engineering, widely used but never confirmed by Rockstar. 🔴 — folklore: repeated in tutorials, plausible, untested — verify before relying on it. ✅ — confirmed in-game on a real working pack. Full legend and corrections log.

Provenance for this part. Everything unmarked below is read straight out of the Cfx client source (citizen-mod-loader-five, rage-device-five, glue, asi-five) at master, and cross-checked byte-for-byte against three shipped client-side packs: CoreFX 1.2 Legacy (FiveM), CoreFX Roads, and GreyNavigator's Realistic Tracers (Clientside). Where a claim is an inference rather than a read, it is marked and said so.

28. The four client-side load paths

§10 covers what a server streams to everyone. This part covers what you load on your own machine, that nobody else sees: the FiveM equivalent of RAGEMP's user_resources/global/game_resources/dlcpacks/.

FiveM does not read the GTA V install's mods/ folder, and it does not read dlclist.xml. It reads four locations of its own, all relative to the FiveM application data root:

%LOCALAPPDATA%\FiveM\FiveM.app\        (default install)
<portable FiveM folder>\FiveM.app\     (portable install)

That folder is cfx:/ internally (it is MakeRelativeCitPath("")); it is the one containing citizen/, data/ and CitizenFX.ini.

Folder What it takes Turned off by
mods/*.rpf A mod package: an OIV-style assembly.xml + content/ tree packed as an unencrypted RPF7. Handles both file replacements and whole add-on DLCs. This is the one people mean by "client-side mod". sv_pureLevel 2 entirely; sv_pureLevel 1 unless the pack is Cfx-signed
addons/*.rpf A raw RPF containing platform/ and/or common/ folders, overlaid onto platform:/ and common:/. No assembly.xml. GTA V only, not RedM. sv_pureLevel 1
citizen/dlc/<deviceName>/ A loose folder that overlays an already-mounted DLC device by name, plus <deviceName>CRC/ for its CRC sibling. Used by FiveM itself; usable by hand. sv_pureLevel 1
plugins/*.asi ASI plugins. See §32. sv_pureLevel 2

None of these folders is created for you except plugins/. If mods/ does not exist, make it.

What does not work (and why the guides say it does)

  • Dropping a modified update.rpf, x64a.rpf or RESIDENT.rpf straight into mods/ does nothing. The loader enumerates mods/, opens each .rpf, and looks for assembly.xml at its root. A package with zero parsed <add> entries is never mounted, silently, with no console line. Bare replacement archives have no assembly.xml, so they sit there inert. This is the single most-repeated piece of bad advice about FiveM client mods, and it is a leftover from the FiveReborn era.
  • Editing dlclist.xml is pointless. FiveM never reads it on this path. A client-side DLC is registered by being listed in an assembly.xml, and its device name comes from its own setup2.xml.
  • OpenIV cannot be involved at runtime. openiv.asi in plugins/ is a hard FatalError, by name and by PE OriginalFilename, so renaming it does not help.
  • Nothing here is encrypted-RPF friendly. The loader accepts only two values in the RPF7 encryption field: OPEN and CFXP. An NG-encrypted archive (which is what most community packs ship) is refused before anything else happens. See the OPEN requirement.

29. The mod package format

A file in mods/ is an OIV package in an RPF container. The same assembly.xml that OpenIV reads out of a .oiv (which is a zip) is read by FiveM out of an .rpf. That is why a mod that ships for both usually ships the same content twice:

CoreFX_Legacy_Singleplayer/CoreFX/CoreFX.oiv          39,001,206 bytes  (zip)
CoreFX_Legacy_FiveM/CoreFX/aa_corefxPack.rpf         129,001,472 bytes  (RPF7, OPEN)

Minimum viable archive layout, taken from the real entry list of the Realistic Tracers pack. Four entries and nothing else:

/                                    (root directory entry)
/assembly.xml                        deflate-compressed
/content/                            (directory entry)
/content/shaders_cave_particles.rpf  STORED UNCOMPRESSED, magic RPF7, encryption OPEN

The OPEN requirement

The header check is two conditions and there is no third path:

  • magic must be RPF7 (0x52504637), and
  • the encryption field must be OPEN (0x4E45504F) or CFXP (0x50584643).

Anything else (NG, AES) is rejected with only non-encrypted RPF7 is supported in the log. This applies to the payload as well as the wrapper: when the loader opens a dlc.rpf you shipped inside content/, it goes through exactly the same check. So a client-side pack is unencrypted end to end. If your source pack is NG (most downloaded packs are), re-save it as OPEN in OpenIV before wrapping it.

CFXP is not an alternative you can produce: it is an RPF7 with a 256-byte RSA signature appended after the name table, validated against a Cfx public key. It exists so Cfx can whitelist specific graphics mods under pure mode. You cannot sign your own.

assembly.xml, as FiveM parses it

FiveM's parser is much narrower than OpenIV's. It reads exactly this and ignores the rest of the format:

<?xml version="1.0" encoding="UTF-8"?>
<package version="2.1" id="{5002C7EC-4476-46A0-B5A7-6FF7F9F06825}" target="Five">
<metadata>
  <name>shaders_cave_particles</name>
  <version><major>1</major><minor>0</minor></version>
  <author><displayName>CitizenFX Collective</displayName></author>
  <description><![CDATA[Revive the sculpture on the beach.]]></description>
</metadata>
<content>
  <add source="shaders_cave_particles.rpf">update\x64\dlcpacks\shaders_cave_particles\dlc.rpf</add>
</content>
</package>

Rules that actually bite:

  • The root element must be named package and carry target="Five". A wrong target logs Failed to parse mod package - target != Five and stops.
  • The five metadata fields above are dereferenced without a null check: metadata/name, metadata/version/major, metadata/version/minor, metadata/author/displayName, metadata/description. Omit one and you are relying on undefined behaviour, not getting a friendly error. Fill all five, even with placeholders.
  • id is parsed as a GUID with the first character skipped, i.e. the braces form is what is expected. It becomes the mount name (modVfs_<guid>:/) and the streaming tag, so two packages sharing a GUID collide. Generate a fresh one per pack.
  • Inside <content>, only <archive> and <add> mean anything. <xml>, <delete>, <text> and the rest of the OIV vocabulary are skipped without comment, which is exactly why a pack can carry a dlclist.xml edit for the singleplayer installer and still behave correctly under FiveM.
  • <archive> nests, and the loader only ever looks at the innermost archive path when deciding where an <add> lands.
  • One package ID is hard-blocked under pure mode: {62AB8F34-BE20-46D5-9F0F-84729F087E5E}, an outdated NaturalVision Evolved weapon add-on with a bad clip size.

The two shapes of an <add>

Whether an entry is a file replacement or a whole DLC is decided by one thing: whether it sits inside an <archive>.

Shape Meaning
<add> inside <archive> Replacement. The source file overrides one path in the mounted game filesystem.
<add> directly under <content> Pseudo-DLC. The source file is treated as a complete dlc.rpf and mounted like an add-on pack. The target path text is ignored entirely; it only exists for singleplayer installers.

Replacement: how a target path is mapped

Backslashes become slashes and a leading slash is stripped, then the innermost <archive path> decides the rewrite:

Innermost archive Target rewrite
update\update.rpf x64/…platform/…; dlc_patch/…dropped; anything else (i.e. common/…) kept as written
x64a.rpfx64w.rpf platform/<target> (matched as: 8 characters, starts x64, .rpf at index 3)
common.rpf common/<target>
anything else silently ignored

That last row is the trap. An <archive path="x64\audio\sfx\RESIDENT.rpf"> block, or a nested archive inside update.rpf whose innermost path is not one of the three forms above, produces no override and no warning. The mapped entries are exposed through a virtual device that is then mounted over common:/, commoncrc:/, platform:/ and platformcrc:/.

Three targets are refused by name even when the mapping is correct:

common/data/gameconfig.xml
common/data/ai/scenarios.meta
common/data/ai/conditionalanims.meta

These are the files whose replacement most reliably breaks a multiplayer session, so the loader drops them. A pack shipping a modified gameconfig.xml for singleplayer is not broken; that entry is just inert under FiveM.

A special case sits above all of this: if the target path ends in .rpf, the file is not treated as an override at all. It is opened as an archive and every file inside is registered individually as a streaming asset. If any of them is a .ymf, the pack also queues a CFX_PSEUDO_CACHE load; if any is a .ybn or .ymap, it queues a map-store reload. This is how a replacement pack ships a whole nested archive of models.

Load order

The mods/ directory listing is collected into a case-insensitive sorted set, so packs load in alphabetical order by filename, and a later pack wins a collision. This is why real packs ship with sort prefixes rather than descriptive names:

mods\aa_corefxPack.rpf     <- loads first
mods\ao_corefxRoads.rpf    <- loads after

Only files are considered, only at the top level, and only those ending .rpf. Subfolders inside mods/ are not walked.

The <order> value in a pseudo-DLC's setup2.xml is never read. The loader means to sort pseudo-DLCs by it, but the guard tests the freshly-zeroed order variable instead of the XML element it just fetched, so the assignment never runs and every pack sorts as order 0. The sort is stable, so the effective order is again the alphabetical filename order. Treat the filename as your only load-order control. (Read from ModVFSDevice.cpp at master; it reads as an upstream bug rather than an intent, and it could be fixed at any time, so do not build a pack that depends on order being ignored either.)

When more than one package loads, FiveM draws N mod packs loaded in the bottom-left of the frontend. That watermark is the quickest confirmation that your file was picked up at all.


30. Shipping a DLC pack client-side

This is the case that matters for clothing, tattoo, prop and weapon packs: you already have a working dlc.rpf (the same one you would drop in dlcpacks/ for singleplayer, or in user_resources/global/game_resources/dlcpacks/<name>/ for RAGEMP) and you want it on your own client in FiveM.

You do not convert the pack. You wrap it. The dlc.rpf goes in byte-for-byte.

The wrapper

bastardpack.rpf              RPF7, encryption OPEN
├─ assembly.xml              (compressed is fine)
└─ content/
   └─ dlc.rpf                STORED UNCOMPRESSED, RPF7, encryption OPEN
<?xml version="1.0" encoding="UTF-8"?>
<package version="2.1" id="{PUT-A-FRESH-GUID-HERE}" target="Five">
<metadata>
  <name>bastardpack</name>
  <version><major>1</major><minor>0</minor></version>
  <author><displayName>you</displayName></author>
  <description><![CDATA[client-side clothing pack]]></description>
</metadata>
<content>
  <add source="dlc.rpf">update\x64\dlcpacks\bastardpack\dlc.rpf</add>
</content>
</package>

The nested .rpf must be stored uncompressed. The RPF entry's compressed-size field has to be 0; the loader mounts a nested archive by seeking into it, so a deflated one cannot be read. Verified on the Realistic Tracers pack: entry size field 0, uncompressed size 14,525,952. This is the same rule that applies to nested archives inside a normal dlc.rpf (§4).

Add a sort prefix to the wrapper's filename if load order against your other packs matters.

Nothing else changes. The dlc.rpf is not repacked, so none of the resource-header hazards of §4 apply; to the wrapper it is one opaque binary entry. That also means a pack that already works in singleplayer or RAGEMP carries over unchanged, and one that was already broken stays broken.

The wrapper is just a prefix. Because the payload is the last thing in the file and goes in verbatim, the bytes in front of it depend only on the pack's name and its length, not its contents. That is 1 KB: a 16-byte header, four 16-byte entries, a padded name table, and the deflated assembly.xml at offset 512, with the pack starting at 1024. So wrapper = prefix ++ pack, exactly, and you can build one by concatenation without a tool that understands RPF at all.

Defragment first: most packs are half empty

An RPF does not shrink when you change what is inside it. Replace a texture with a smaller one, or delete a file, and the blocks it occupied stay allocated; the archive simply stops referring to them. Every edit in OpenIV or CodeWalker leaves another hole, and a pack that has been through a few revisions is routinely more free space than content. Measured on four real community packs:

Pack As shipped Defragmented Reclaimed
a weapon pack1595.7 MB769.2 MB52%
a clothing pack425.1 MB352.0 MB17%
a helmet pack152.8 MB6.8 MB96%
a vest pack7.4 MB7.4 MB0%

Defragmenting rewrites the archive with its files packed end to end. It removes nothing, renames nothing and re-encodes nothing. Verified on the 152.8 MB pack above: 104 files before and after, every file's bytes identical, and no entry changed between resource and binary (that last one is the check that matters, because a resource silently demoted to binary is the ERR_SYS_INVALIDRESOURCE_5 class from §11).

CodeWalker's RPF Explorer has a Defragment command. Run it before wrapping: the wrapper has to carry every byte of slack the pack is holding, and on the packs measured above that is most of the file.

Two things it will not reclaim, in either tool. A nested archive that is compressed has to be inflated before it can be rebuilt, so it is copied whole and keeps its own gaps. And an entry of 16 MB or more stores 0xFFFFFF in its 24-bit size field, so its true length is not in the index at all; what is recoverable is its block extent, from where the next entry starts, which is enough to move it correctly but leaves any hole directly after it in place.

Size ceilings

Two different limits, often confused:

  • The format's is 4,294,966,784 bytes (0x7FFFFF × 512), which is 4 GB less 512 bytes, not 4 GiB. The wrapper has to hold the whole pack inside one archive, so it is bound by the same ceiling as any other RPF (§1). Exceeding it corrupts silently rather than failing. An entry gets 23 bits of offset because the 24th is the flag that says whether the entry is binary or a resource, so a file written past 4 GB sets that bit and changes its own type on the way out; the header still parses as valid RPF7 and the archive looks fine until something inside it turns out to be missing. No amount of editing entry bytes fixes it; nothing can address past 0x7FFFFF blocks. Split the pack into sub-packs (dlc.rpf, dlc1.rpf, …) with subPackCount in setup2.xml, the way the base game ships mpheist4, and wrap each one, but see the sub-pack warning under §33, because FiveM's mod loader does not mount them the way the game does.
  • Memory is the browser's, and it is what decides how the file gets built rather than whether it can be. Above roughly 512 MB the builder stops loading the pack at all: it reads a few KB of index, works out where every file should land, and writes the result straight to disk as a list of copy instructions. Defragmenting survives that, because defragmenting re-encodes nothing: every file's bytes move unchanged and only the offsets in the index change, so it can be done as a stream. A 1.6 GB pack came out at 769 MB this way with all 283 files byte-identical.

Reading free space out of the index has one trap. An entry's size lives in a 24-bit field that RpfResourceFileEntry.Write clamps to 0xFFFFFF (§1), so any file of 16 MB or more reads back as exactly 16 MB and its true length is not in the table of contents at all. Measured across 492 real resource entries, CodeWalker's own size agrees with the field on all 483 unclamped ones and differs on exactly the 9 clamped ones. The page-flag words are not the missing number: they describe the in-memory page layout and matched the on-disk size zero times out of 492.

The recoverable quantity is the block extent: the next entry begins where this one's blocks end. On those nine clamped entries the gap ran 42–433 bytes past the true size every time: exactly the padding to the next 512-byte boundary, which is what occupancy counts anyway. Bounding a clamped entry that way takes the free-space figure from 8% high to exact on all four packs measured. Only a clamped entry with nothing after it in its own archive stays unbounded, and then the figure is reported as an upper bound rather than a number.

What FiveM reads out of the pack, and what it ignores

Once mounted, the pseudo-DLC path reads two files out of your dlc.rpf:

setup2.xml yields three fields, and only three:

Field Effect
deviceName The pack is mounted at <deviceName>:/. This is the collision surface: two packs declaring the same device name (the autogenerated dlc_pck is a common offender) fight, and one of them silently loses. Same failure class as §17.
requiredVersion A build gate. A plain integer means "this build or newer". Cfx adds a range form, min-max, e.g. 3095-3407. Outside the range the pack is dropped, quietly.
order Read but discarded; see §29.

Everything else in setup2.xml (type, isLevelPack, contentChangeSetGroups, startupScript) is not looked at on this path.

In content.xml, only /…/dataFiles/Item is walked, taking filename and fileType from each:

  • %PLATFORM% in a filename is substituted with x64.
  • fileType of RPF_FILE (exact case) → the archive is mounted and its top-level entries registered as individual streaming assets. Only the top level. The enumeration is a single FindFirst/FindNext pass at the mount root that explicitly skips anything flagged as a directory, and it does not recurse, so a file in a subfolder is silently never registered. That is where every per-ped component set lives, which is why this particular routing cannot deliver clothing (see the caret rule below). The same ceiling applies to the .rpf-target intercept in the replacement branch. The case-sensitive match matters too: a case variant escapes to the game's real mounter, which does recurse into the subfolders, but that mounter is called non-overlay and overrides nothing. Tested and closed at the vanilla-mounter route.
  • Every other fileType (SHOP_PED_APPAREL_META_FILE, HANDLING_FILE, DLC_ITYP_REQUEST, …) → queued through the ordinary data-file loader, the same one a server resource's data_file directive uses.

<contentChangeSets> is not read at all. Every dataFiles entry loads unconditionally, regardless of which changeset would have enabled it, what group that changeset belongs to, or whether any changeset names it. So on this path:

  • changeset name collisions between merged packs stop mattering;
  • filesToEnable scoping stops mattering;
  • filesToDisable and filesToInvalidate stop working: a pack that relies on disabling a vanilla file will not do so;
  • a mapChangeSetData block is inert, so map packs that mount through changeset payload rather than plain dataFiles need testing rather than assumption. 🟡

Everything in §4 about content/setup2 still describes the file you must author; it is only the consumption that narrows here.

One more thing about registration, because it closes off an obvious idea: a file inside a faux-streamed archive is registered under its own bare name, run through the same GetBaseName that decodes carets. Putting caret-named files inside the payload rather than loose in the wrapper changes nothing; it is the same flat, policy-gated registration either way.

Clothing packs: add-on works, replacement does not

These are two different things and only one of them is possible here.

Add-on clothing, a pack that ships new drawables at the root of its own x64/models/cdimages/<archive>.rpf, registered by flat name and selected through a SHOP_PED_APPAREL_META_FILE, is handled on this path: the meta through the data-file loader, the drawables as streaming assets. 🟡 Still an inference from the loader source, not confirmed in-game, so treat a first pack as a test. Note it only gives you new component IDs, and on a server you do not own you cannot select component IDs the server does not know about.

Replacing a base freemode component, swapping head_000_r.ydd, uppr_000_r.ydd, teef_004_u.ydd and the like for your own models, works with one condition and one packaging rule.

  1. The pack must be flattened to caret form. Wrapping the dlc.rpf whole is a dead end: ped component files live in a <collection>/ subfolder, and the faux-streaming mount enumerates only the top level, so they are never seen. Emit each asset loose into content/ named <collection>^<file> instead, the shape described under the caret rule.
  2. The server must grant subdir_file_mapping, which is force-granted below 11 slots and otherwise depends on the server key. ✅ Verified working on an 8-slot server and verified doing nothing on a 48-slot one, same pack either way.

Everything §16 says about authoring the pack itself still applies; it is the delivery that is blocked.

The caret rule, and the server policy that switches it on

A ped component is replaced by naming the file mp_f_freemode_01^uppr_000_r.ydd. The ^ stands in for the folder separator, naming the ped dictionary the file belongs to. It is the same convention a server resource uses in its stream/ folder, and it works from the mods folder too, but only when the server grants a policy, which is the part nobody writes down.

The caret is decoded in exactly one function, GetBaseName in gta-streaming-five/src/LoadStreamingFile.cpp:

static std::string GetBaseName(const std::string& name)
{
    std::string retval = name;
    std::string policyVal;
    if (Instance<ICoreGameInit>::Get()->GetData("policy", &policyVal))
    {
        if (policyVal.find("[subdir_file_mapping]") != std::string::npos)
        { std::replace(retval.begin(), retval.end(), '^', '/'); }
    }

The resulting slash is what arms ped registration further down the same file, which sets CPedModelInfo::streamFolder so the game asks the streamer for mp_f_freemode_01/uppr_000_r instead of pulling the component out of the packed per-ped directory. Without the slash the lookup misses every vanilla slot and mints an orphan slot nothing ever requests: registration reports success, nothing is logged, and nothing changes.

policy comes from the server during the connect handshake, out of the server key's entitlements, and net/src/NetLibrary.cpp:1407 force-grants subdir_file_mapping to any server with sv_maxclients of 10 or fewer, on the reasoning that those are development and testing servers. There is no client-side way to set it.

Measured both ways on build 3751. The same pack, the same mods folder, the same client:

sv_maxclients Policy Caret-named ped components
48not grantedSilently do nothing: pack loads, watermark counts it, log is clean, no change in game
8force-grantedModels appear. Replacement heads and bodies show. Their textures do not (see below)

So the policy is necessary but not sufficient. It fixes the name; it does not help an asset whose registration already happened. That distinction splits ped replacement cleanly in half.

What needs no policy at all, which is most things. Any streaming asset whose name is globally unique and sits at the top level of its archive carries no caret, hits the real slot, and replaces cleanly on any server, large or small:

  • Roads, terrain and map textures: the .ytd/.ydr pairs behind road surfaces, kerbs and props. Unique names, top level, no prefix. This is the single largest category that works client-side, and it works everywhere.
  • Vehicle assets: elegy.ytd and friends, liveries, light textures.
  • Licence plates: vehshare.ytd.
  • Face overlays: mp_fm_faov_*.ytd, which sit at the archive root rather than inside a ped folder, unlike the component textures below.

Verified in-game with the policy absent: plates, vehicle lights and map textures all changed. Note these are .ytd files loading in the pre-connect Startup phase and working perfectly, because the phase was never the problem on its own. The problem is only ever a name that needs decoding before it means anything.

The .ytd exception: ped textures never arrive

⚠️ Through every routing this page tests, client-side ped textures cannot be replaced from the mods folder, on any server, at any slot count. Models can, on a policy-granting server. This is a FiveM ordering bug, not a setting, and it is worth understanding before spending days on a pack that will only ever be half-applied. (The case-variant vanilla-mounter route sidesteps the ordering entirely; it was tested and closes even harder, because it mounts non-overlay and overrides nothing at all, models included.)

LoadStreamingFiles runs at several init phases and drains the pending set as it goes. Mods-folder files are eligible at Startup and BeforeMapLoad, but Startup takes only two extensions, and consumes them:

if (loadType == LoadType::Startup)
{
    if (ext != "ytd" && ext != "gfx")
    {
        ++it;
        continue;              // everything else waits for a later phase
    }
}
...
it = g_customStreamingFiles.erase(it);      // consumed once, no second chance

The two phases are hooked to different points in the game skeleton:

PhaseInit hookRelative to connectHandlesCaret decodes?
StartupINIT_COREBefore the handshake.ytd, .gfxNo; no policy yet
BeforeMapLoadINIT_BEFORE_MAP_LOADEDAfter the handshakeeverything else, incl. .yddYes

So a texture dictionary is registered at INIT_CORE under its literal name (mp_m_freemode_01^teef_diff_004_a_uni.ytd, carets and all, a name nothing ever requests) and then erased from the pending set before the phase that could have decoded it. The drawable beside it waits, gets decoded, and works. That is the whole of the observed asymmetry.

Measured. Forcing .ytd to defer out of Startup moved the crash-time TxdStore occupancy from 405 to 129, i.e. 276 mods-folder texture dictionaries really are being consumed in that pre-connect phase.

Four escape routes look plausible on paper, and none survives testing:

  • Ship the texture under a different tag so it skips Startup. The eligibility check at the top of the loop admits exactly mod_ and faux_pack prefixes, which is precisely what every mods-folder file is tagged with, whether it arrives through the direct-mount branch or the faux-streaming one. There is no third tag to reach for.
  • Drop the caret so no decode is needed. The prefix exists because these names are not unique. Checked against a stock build 3751 install: of 24 assets in one vest pack, 0 were unique and all 24 collided; teef_diff_004_a_uni.ytd alone appears under 20 ped folders, male and female, base plus every heist/luxe/2023/2024 variant. A bare name is genuinely ambiguous.
  • Wrap the pack as a pseudo-DLC so the game mounts it natively. A RPF_FILE entry routes to the faux-streaming mount, which enumerates one directory level and skips directories, so the per-ped folders are never seen. A case variant does reach the game's real mounter and mounts the subfolders correctly, but FiveM calls that mounter with overlay=false, so the replacement never overrides the base game. Tested and closed at the vanilla-mounter route.
  • Embed the textures in the .ydd. Technically possible via the drawable's shader-group dictionary, but a component drawable carries a whole row of texture variants; embedding collapses them to one. Fine for a single-look prop, wrong for clothing.

A fifth route, the case-variant fileType that bypasses the faux mounter and reaches the game's own packfile mounter, looked like it might sidestep the whole problem. It was built, tested in-game, and closed: the mount succeeds, but FiveM mounts it non-overlay, so it overrides nothing.

Patching FiveM itself to move the phase is also a dead end, and not one worth pursuing: FiveM ships a closed-source integrity component, and modifying gta-streaming-five.dll in memory terminates the process through a deliberate trap (reported as Early-exit trap, an execute fault in a guarded page near the game module base). Two independently-correct patches both hit it. Treat FiveM's own modules as off-limits. A hook on the game module is a different story, and it is what the texoverride route below is built on.

What actually works: replace the asset in the vanilla archive it currently ships from, and re-encrypt (ArchiveFix, or CodeWalker writing the archive back). No caret and no policy are involved; the game loads it natively from the right folder. But which archive is not obvious, and getting it wrong is the single most common reason a re-encrypted replacement changes nothing (see the winning archive below). The trade-offs are real: game updates and store file-verification revert it, sv_pureLevel 1 or higher rejects it, and a modified install must never be taken into GTA Online.

The clean alternative, where you control the server: put the same caret-named files in a resource's stream/ folder. Server-streamed assets are skipped at Startup precisely because they are not mods-folder tagged, so they land in a phase where the policy exists, and they reach everyone connected, not just you.

The texoverride route: an ASI hook instead of a package

Everything above is packaging, and packaging cannot fix the .ytd ordering. An ASI plugin can, because it never enters the mod loader's phases at all. texoverride (open source, MIT) hooks registerRawStreamingFile, the routine FiveM itself uses to register loose and server-streamed files, located by the same byte pattern the Cfx source uses. With the hook in place it reads loose .ydd and .ytd files from plugins/tex_overrides/<collection>/ and routes them over the originals. Nothing is renamed, no assembly.xml is written, and no archive is edited.

It handles the two ways clothing ships differently:

  • Streamed DLC collections: the hook redirects on an exact collection/file match, so the replacement lands under the name the game actually requests. No caret encoding, no subdir_file_mapping policy, and no Startup drain, because the interception sits on the registration path itself rather than in a pre-connect phase.
  • Base freemode files (the ones served from x64v.rpf): the plugin registers the loose file itself under the base slot name, which takes precedence over the archive copy.

The constraints are the ASI loader's (§32), plus its own scope:

  • sv_pureLevel 0 servers only. Level 1 demands a Cfx signature a self-built ASI cannot have, and level 2 turns the ASI loader off entirely.
  • Rebuilt per game build. The FX_ASI_BUILD resource has to name the running build, so the plugin goes dead on every game-build update until it is rebuilt.
  • Human freemode peds only, .ydd and .ytd only, and the collection folder must exactly match one of the 186 valid collection names (the repo's COLLECTIONS.md lists them).

On risk: the patch is one inline hook of about five bytes on a cosmetic asset-routing function, plus MinHook's trampoline page, and it touches no gameplay system. A generic code-integrity scan can still flag the patch regardless of intent, and Cfx's tolerance of game-module hooks is practice, not a written guarantee. The integrity trap described above guards FiveM's own modules; this hook lands outside them, which is why it runs where the phase patches died.

The winning archive is per-file, not per-pack

Measured across a full install (92 dlcpacks + update.rpf + x64v.rpf, build 3751) while getting an HD freemode-appearance pack to load. "Replace it in the vanilla archive" hides a real trap: an appearance asset is shipped and re-shipped across many DLCs, and only the last-loading copy is the one the game reads. Load order is the <order> value in each pack's setup2.xml (higher = later = wins; ties break on minorOrder, then dlclist.xml position), not folder name, and not the "mppatches overrides everything" folklore. Editing any archive but the winner is silent: the file re-encrypts fine, the game loads the other copy, nothing changes.

Three things make "the winner" narrower than a whole pack:

  • It is resolved per file, not per component. A patchday pack often re-ships only a handful of specific drawable numbers. patchday27ng winning lowr_004 says nothing about lowr_000, which a lower pack still serves. Resolve the exact filename.
  • It differs by gender. The same slot resolves to different packs for mp_m_freemode_01 and mp_f_freemode_01: one measured install served male uppr_000_r from patchday9ng but female uppr_000_r from patchday4ng, and female lowr_000_r from patchday17ng. Patch the gender you play.
  • The model and its texture can live in different archives. A drawable (.ydd) and its diffuse (.ytd) for the same slot are not guaranteed to travel together; one measured pair had the model in patchday9ng and the skin in mppatchesng.

Body skin is not uppr_diff. The freemode ped's bare torso, arms, legs and feet skin is textured by loose, root-level dictionaries named mp_fm_skin_<m|f>_<up|lo|fe>_<tone>.ytd (up upper, lo lower, fe feet; tones whi bla lat chi ara pak) sitting alongside the face overlays (mp_fm_faov_*) and mp_eye_colour.ytd in a ped_mp_overlay_txds.rpf. These carry globally unique names at an archive root, so unlike the head models they also replace cleanly straight from the mods/ folder with no policy (the same category as plates and face overlays). If a "body texture" refuses to change, check you are editing mp_fm_skin_*, not a component _diff.

One dlc.rpf can hold two ped archives at different depths. A single appearance patch (e.g. patchday4ng) commonly nests the head/component drawables in x64\models\cdimages\<pack>.rpf\mp_?_freemode_01\ and the skin/overlay dictionaries in a separate x64\models\ped_mp_overlay_txds.rpf at the root of models\, not under cdimages\. Replacing one nested archive and not the other is exactly the "heads updated, body still vanilla" symptom: both live in the same outer dlc.rpf, so you re-encrypt once but must edit both.

To find the winner without guessing: enumerate every dlcpacks/*/dlc.rpf (plus dlc1.rpf sub-packs, update.rpf, and x64v.rpf), read each setup2.xml <order>, and for your exact filename keep the highest-order archive that contains it. CodeWalker's global search plus a glance at each pack's order does the same by hand.

The vanilla-mounter route: a fifth escape, tested and closed

Tested in-game on build 3751 (2026-08-17): the mechanism fires exactly as predicted, and it still does not deliver clothing, for a new reason, one line deep. The four dead ends above share one assumption: that a pseudo-DLC's streamed archive is doomed to the one-level faux mounter. That part is wrong. The routing is a single case-sensitive string compare, and the game's own packfile mounter sits one branch away. Reaching it changes nothing, because of how FiveM calls that mounter.

When FiveM walks a payload's content.xml, each dataFiles item is dispatched on its fileType text (ModVFSDevice.cpp):

if (fileType == "RPF_FILE")                       // exact, case-sensitive
    MountFauxStreamingRpf(filename);              // one directory level, subfolders lost
else
    streaming::AddDataFileToLoadList(fileType, filename);   // -> the game's real mounter

The else branch is the interesting one. AddDataFileToLoadList hands the type to LookupDataFileType, which upper-cases the string before hashing it (HashRageString(boost::to_upper_copy(type))). So a case variant (rpf_file, Rpf_File, anything that is not the exact bytes RPF_FILE) does two things at once:

  • it misses FiveM's == "RPF_FILE" intercept, skipping the faux mounter entirely; and
  • it still resolves to the same enum once upper-cased. HashRageString("RPF_FILE") is 0x04DF4461, which is index 0 in the game's data-file-type table, and LookupDataFileMounter maps type 0 to g_staticRpfMounter, the CfxPackfileMounter that calls the game's real _addPackfile.

That real mounter is the one every stock clothing DLC goes through. It registers the whole archive, subfolders and all, so mp_m_freemode_01/teef_diff_004_a_uni.ytd mounts by its true path inside a real device. No caret, because the folder is a real folder, not a name that needs decoding. No subdir_file_mapping policy, for the same reason. And it runs at INIT_SESSION, after the handshake, so the Startup drain that erases mods-folder .ytd never touches it. It is also the exact mounter a server resource's data_file 'RPF_FILE' directive uses, so the code path is supported, not a trick that happens to compile.

Packaging is otherwise the ordinary pseudo-DLC wrap of §30, with two specifics:

  • The payload dlc.rpf must be OPEN as always, so FiveM can read its content.xml, but the nested streamed rpf (streamedpeds_mp.rpf and the like) can stay NG-encrypted, because it is the game's mounter, not FiveM's, that opens it, and the game has the keys.
  • The only edit to a working pack is the one token in content.xml: <fileType>RPF_FILE</fileType><fileType>rpf_file</fileType>. Same length, same meaning to the game, invisible to FiveM's intercept.

What the test showed. The log confirms the branch: loading rpf_file dlc_h4vests:/…/streamedpeds_mp.rpf … done loading, through the game's real data-file mounter, no error. The archive mounted, subfolder and all. And nothing changed on the ped, models or textures.

The reason is one commented-out line. FiveM reaches the real mounter through CfxPackfileMounter::LoadDataFile, which builds the packfile entry like this:

bool CfxPackfileMounter::LoadDataFile(CDataFileMgr::DataFile* entry)
{
    entry->disabled = true;
    //entry->persistent = true;
    //entry->locked = true;
    //entry->overlay = true;      // <- commented out
    _addPackfile(entry);

The entry arrives memset to zero and only its name and type are set, so overlay is false. An overlay packfile is the mechanism by which a DLC replaces a base-game file: it is what makes mp_m_freemode_01/teef_004_u from your archive win over the stock one. Mounted without it, your drawables are merely added alongside the base game's, and for a component slot the game already fills, the base entry keeps winning. So a base-component replacement pack mounts flawlessly and overrides nothing. That flag is set in FiveM's C++, not in your content.xml, so no packaging change can turn it on. (The ExtraContentManager change-set flow is skipped here too, GROUP_STARTUP and filesToEnable are never read, but the overlay flag is the nearer wall: the pack does not even get the chance to fail at enablement.)

So the fifth escape is closed, and it closes more completely than the others: models and textures alike, because the whole archive fails to override, not just the .ytd. It would still add new drawables at unused component indices (add-on clothing, not replacement), but on a server you do not own you cannot select an index the server does not know, which is the same ceiling the caret route hits. For replacing what freemode already ships, the real answers stand: a server resource's stream/ folder, a re-encrypted vanilla-archive replacement, or the texoverride hook on a sv_pureLevel 0 server.

Versus RAGEMP (legacy)

RAGEMP is treated as a legacy platform here. The two client-side targets that matter now are FiveM and singleplayer; the RAGEMP dlcpacks workflow still functions but is no longer where this reference points people. The comparison below is kept because a great many existing clothing and tattoo packs were authored for RAGEMP and get carried across: read it as "how the thing you already have maps onto FiveM," not as a recommendation to start on RAGEMP. (This is a scope note, not an evidence tier; the markers on this page rate confidence, not platform status.)

RAGEMP FiveM
Location user_resources/global/game_resources/dlcpacks/<name>/dlc.rpf FiveM.app/mods/<name>.rpf
Unit One folder per pack, the pack as-is One wrapper archive per pack, pack inside
Registration RAGEMP's own dlclist handling assembly.xml + the pack's own setup2.xml
Encryption NG packs work OPEN only, wrapper and payload
Order control Folder order / dlclist order Alphabetical filename
Server can block it No equivalent Yes, via sv_pureLevel
Ped component model (.ydd) Works; that is what these packs are for Works on a server granting subdir_file_mapping (force-granted at 10 slots or fewer). Pack must be flattened to caret names. Inert on a large server
Ped component texture (.ytd) Works Never works: drained pre-connect, see the .ytd exception
Roads, map, vehicles, plates Works Works; unique names need no policy, any server

A RAGEMP dlcpacks clothing pack can be reshaped into a client-side FiveM pack, but the change is a flatten, not a wrap: every asset comes out of its nested archive and goes loose into content/ as <collection>^<file>. Understand what that buys you before doing it. On a server granting the policy the models appear and their textures do not, so a clothing pack lands half-applied: new silhouette, vanilla skin. On a large RP server nothing lands at all.

For clothing specifically, the routes that actually finish the job are a server resource's stream/ folder if you run the server, a re-encrypted vanilla archive replacement, or the texoverride ASI on a sv_pureLevel 0 server. All three are covered under the .ytd exception.

Moving a whole RAGEMP set across is not a mechanical job

⚠️ A pack loading is not the same as a pack being right. Everything on this page describes how the client-side loader resolves files; none of it says the result will behave. Taking a large multi-pack RAGEMP environment set over to the mods folder produced assets that mounted cleanly, passed every structural check, and then misbehaved in world: vehicle parts drifting and moving with no input among them. Structural validation cannot see that: an archive can be a byte-perfect OPEN RPF7, every entry a correctly-typed resource, every name unique, and the content still be wrong for the engine it has landed in.

Treat a whole-set move as a porting job with in-game testing per pack, not a repackage. The reasons a set can be built for one client and misbehave in another are outside the archive format: different base game builds and patch levels, physics and fragment data authored against a different vehicle set, load order that is explicit in a dlclist and merely alphabetical in the mods folder, and metadata the other platform's loader read that FiveM's never looks at. Convert one pack, look at it, and only then convert the next.

Audio: shadowing a vanilla wave pack

Replacing built-in game audio (weapon fire, impacts, engine notes) looks like it should be a file replacement, and every route that treats it as one fails:

  • A mods/ package with <archive path="x64\audio\sfx\WEAPONS_PLAYER.rpf"> is the trap row of the mapping table above. Not one of update.rpf, x64a..x64w.rpf or common.rpf, so it is dropped silently, with no console line.
  • An addons/ overlay carrying platform/audio/sfx/RESIDENT.rpf mounts once it is encrypted, and still changes nothing.
  • Replacing the archives in the GTA V install works, but it is the install, so a game update reverts it and singleplayer inherits it.

There is a client-side route, and it is not a replacement at all. A DLC wave pack whose container folder carries the same name as a base container shadows it. Ship a pack whose AUDIO_WAVEPACK folder is literally weapons_player, and the base game's own audio config keeps asking for WEAPONS_PLAYER\PTL_PISTOL and resolves onto your bank. No .rel authoring, no name tables, no merging into vanilla config.

The pack is a normal audio DLC, wrapped as a pseudo-DLC like any other:

dlc.rpf
  setup2.xml     deviceName dlc_wepsnd, EXTRACONTENT_COMPAT_PACK, GROUP_STARTUP changeset
  content.xml    one AUDIO_WAVEPACK per container, disabled=true, enabled by the changeset
  x64/audio/sfx/weapons_player/   22 .awc     <- name matches the vanilla container
  x64/audio/sfx/resident/         16 .awc

Ship the container complete. The shadow is per container, not per file: a folder holding five of RESIDENT's sixteen banks takes the other eleven with it, and collision, vehicle and explosion audio go quiet. Extract the vanilla container, overlay the banks you are changing, ship all of them.

The saving is worth noting. Replacing whole archives meant shipping a 150 MB RESIDENT.rpf; shipping only the weapon banks that actually change is 4.3 MB. Cost scales with what you edited, not with what the archive happens to contain.

Two related requirements that are easy to invert, because they are opposites:

FolderEncryption
mods/*.rpfmust be unencrypted (OPEN)
addons/*.rpfmust be encrypted

Get either wrong and the file is ignored with no error. If you generate an RPF with a tool rather than OpenIV, run ArchiveFix over it before concluding the mod does not work; a structurally valid archive that the game still refuses is the usual reason a correct package appears to do nothing.


31. Pure mode

Pure mode is a server setting, sv_pureLevel, that makes the client refuse to run with modified game files. It is a client-file integrity check and nothing more: it is not an anti-cheat, and it says nothing about server-side security.

Level Effect on client mods
0 (default) No validation. All four load paths in §28 are live.
1 Every packfile the game opens is hashed and checked. addons/ and citizen/dlc/ are switched off. mods/ packages must be CFXP-signed, so in practice your own packs stop loading. Audio is exempted (below).
2 Everything level 1 does, plus the mod loader and the ASI loader are disabled outright before they run.

What level 1 actually checks

It does not hash file contents. It hashes the archive's entry table (entryCount × 16 bytes, read straight after the header) through a normalisation pass, then looks the result up in four generated allowlists (base game, update, DLC, and a manual table).

The normalisation walks the table in 16-byte steps and, for each entry that is not a directory (the u32 at +4 is not 0x7fffff00) and has its high bit clear, clamps the non-zero u32 at +12 to 1. The SHA-256 of the normalised table is the identity. The practical consequence: the check is sensitive to the file list, not to file contents. Repacking an archive changes it; so does adding, removing or renaming one entry.

Failure is fatal and names the file:

Invalid modified game files (<path>)
The server you are trying to join has enabled 'pure mode', but you have
modified game files. Please verify your GTA V installation (see
http://rsg.ms/verify) and try again. Alternately, ask the server owner for help.

At level 1 only, four paths skip the check entirely; this is the "audio mods are allowed" behaviour:

x64/audio/sfx/RESIDENT.rpf
x64/audio/sfx/WEAPONS_PLAYER.rpf
x64/audio/sfx/STREAMED_VEHICLES*      (prefix match)
x64/audio/sfx/RADIO*                  (prefix match)

Note that these are paths in the game's installation, not filenames in FiveM's mods/ folder. Copying RESIDENT.rpf into FiveM.app/mods/ does nothing at any pure level; it has no assembly.xml. Audio replacement is done in the GTA V install.

The "known graphics mods still work at level 1" behaviour is the same mechanism from the other side: the manual hash table and the CFXP signature are how Cfx whitelists specific packs. There is no way for a third party to add to either.


32. ASI plugins

FiveM.app/plugins/ is created for you on first launch. Every .asi directly inside it is loaded, non-recursively, unless it trips one of these:

Rule Detail
Name blacklist openiv.asi, scripthookvdotnet.asi, fspeedometerv.asi. Matched against both the filename and the PE version resource's OriginalFilename, so renaming the file does not get past it. openiv.asi additionally raises a fatal error rather than being skipped.
Build stamp On game build 2189 and newer, the DLL must carry a Win32 resource named FX_ASI_BUILD with the ID of the exact running build. Missing it logs "this ASI plugin does not claim to support game build N" and the plugin is skipped. Plugin authors add FX_ASI_BUILD <build> BEGIN "\0" END to their .rc, which means a plugin has to be rebuilt for every new game build; this is why ASIs go dead after a FiveM update.
Managed code Any PE with a COM+ descriptor directory (a .NET assembly) is refused.
Known-bad build gears.asi (manual transmission) with a PE timestamp at or below 0x605FC73B is refused with a message box.
Compat shim pld.asi is loaded then patched at module + 0x1560 to disable a log writer whose missing null check kills the process.

ScriptHookV itself is not loaded from here; the launcher handles it separately, and servers can and do disable plugins. The whole loader is skipped at sv_pureLevel 2.

ReShade is not an ASI and does not go through this path: it loads as dxgi.dll next to the executable, which is why the FiveM build of a graphics mod ships dxgi.dll where the singleplayer build ships d3d12.dll.


Raster minimaps and the bitmap gate

A minimap replacement that works streamed from a server can come across client-side with a blurred radar and a perfect pause map. That is not a packaging fault, and it is worth knowing before rebuilding the pack six ways.

Most replacement minimaps are raster: they supply high-resolution minimap_<x>_<y>.ytd tiles and replace the vanilla .ydd tiles with ~461-byte blanks, deleting the vector road geometry so the image shows through. But the radar only draws the bitmap below a zoom threshold, set in x64/data/tune/minimap.ymt:

<Bitmap>
  <bAlwaysDrawBitmap value="false" />
  <iBitmapTilesX value="2" />  <iBitmapTilesY value="3" />
</Bitmap>
<Camera>
  <fBitmapRequiredZoom value="80" />   <!-- bitmap drawn only BELOW this -->
  <fExteriorFootZoom   value="83" />   <!-- on foot  83 > 80 -> vector -->
  <fVehicleStaticZoom  value="96" />   <!-- parked   96 > 80 -> vector -->
  <fVehicleMovingZoom  value="48" />   <!-- driving  48 < 80 -> BITMAP -->
</Camera>

The diagnostic is free: drive. GTA zooms the radar out with vehicle speed, so a raster minimap sharpens above a certain speed and blurs again when you slow down. That single observation identifies the gate; nothing else produces a speed-dependent blur.

Fix: ship minimap.ymt with bAlwaysDrawBitmap set to true. It is a PSO (PSIN magic, not an RSC resource; convert with the PSO path, not the resource loader), and it goes in as an ordinary vanilla-file replacement under x64\data\tune\minimap.ymt. Raising fBitmapRequiredZoom above every zoom in the file (the largest is the interior at 500) closes the same gate from the other side.

Two dead ends worth not repeating. mapzoomdata.meta governs only the pause map: editing its ZOOM_LEVEL_0/_1 changes the pause map's zoom-out limit and does nothing to the radar, whatever the level names suggest. And SetRadarZoom, which the script-based fixes for these packs call (typically 900–1200, with authors warning not to go below ~840), is a runtime native: the mods folder ships assets, never scripts, so that route does not exist client-side. The tuning flag is the only data-driven answer, and it is the better one: it removes the threshold instead of forcing the radar to stay zoomed past it.

Check iBitmapTilesX / iBitmapTilesY against the tile set while you are in the file: 2×3 corresponds to the six minimap_0_0minimap_2_1 tiles, and a pack whose grid disagrees will not line up.


33. Client-side failure modes

Symptom Likely cause
Pack in mods/, nothing happens, no log line No assembly.xml at the archive root (a bare replacement rpf), or it parsed to zero <add> entries. A package with no entries is never mounted.
only non-encrypted RPF7 is supported The wrapper or the payload dlc.rpf is NG-encrypted. Re-save as OPEN in OpenIV.
Failed to parse mod package - target != Five <package> is missing target="Five" (an RDR2 or GTA IV OIV, or a hand-written file).
it needs to be signed You are on a sv_pureLevel 1 server. Unsigned packs cannot load there; there is no workaround.
Invalid modified game files (…) on join Pure mode, and the named archive in your GTA V install has a modified entry table. Verify the game files.
Pack mounts, watermark shows, content missing For a replacement: the innermost <archive path> is not one of the three recognised forms, so the entry mapped to nothing. For a DLC: the payload's content.xml declares its files only through a changeset, which is not read.
Two packs installed, only one appears Same deviceName in both setup2.xml files, or the same id GUID in both assembly.xml files. Merge them (§17) or rename the device.
Pack works, then dies after a FiveM update A requiredVersion range that no longer contains the new build. It is dropped silently.
Nested archive in the wrapper reads as corrupt The content/dlc.rpf entry was deflated. Nested archives must be stored uncompressed.
ASI silently not loading Missing FX_ASI_BUILD for the current game build, or it is a .NET assembly, or it is name-blacklisted.
Modified gameconfig.xml has no effect It is one of the three targets refused by name. So are scenarios.meta and conditionalanims.meta.
Clothing pack loads, watermark counts it, log is clean, nothing changes Almost always the subdir_file_mapping policy; see the caret rule. Caret-named ped components are inert unless the server grants it (force-granted at sv_maxclients 10 or fewer). Test on a small server before suspecting the pack. The other cause is a pack wrapped whole rather than flattened: assets in a subfolder are never enumerated.
Clothing model changes but keeps the vanilla texture Expected, and not fixable from the mods folder. The .ydd registers after the handshake and decodes its caret; the .ytd registers before it and is erased. See the .ytd exception. There is no packaging fix; the one way around it is the texoverride ASI hook, which works only at sv_pureLevel 0.
Road or map textures work, clothing textures do not, same pack format Not a contradiction. Map assets have globally unique names and need no caret, so the pre-connect phase serves them fine. Only names requiring a subdir prefix are affected.
Re-encrypted a vanilla archive, in-game nothing changed Wrong archive: the file is re-shipped by a later-loading DLC that wins. The winner is resolved per file (and per gender, and model vs texture can split), by setup2.xml <order>, not folder name. See the winning archive.
Head/face textures updated but the body stayed vanilla Two things. The winning archive for body components differs from heads (often a different patchday), and the body skin is mp_fm_skin_* in a ped_mp_overlay_txds.rpf, frequently a second nested archive inside the same dlc.rpf as the head cdimages archive. Edit both nested archives, then re-encrypt once. See the winning archive.
Minimap blurred on foot, sharp while driving; pause map fine A raster minimap hitting the bitmap gate: the radar only draws the tiles below fBitmapRequiredZoom, and speed zooms it out past that. Set bAlwaysDrawBitmap in minimap.ymt. See raster minimaps. Not a streaming fault: a working pause map proves the tiles load.
A sub-packed pack mounts and only part of its content appears FiveM does not mount sub-packs. The game mounts dlc.rpf, dlc1.rpf… under one device; the mod loader opens only the file it was handed, so any dataFiles entry resolving into dlc1.rpf silently fails to mount. Measured on two real environment packs: one had 7 of 16 dataFiles in dlc.rpf and 9 in the sub-pack, the other 1 of 5. Both loaded, neither complained. Check where a pack's dataFiles paths actually resolve before wrapping, and handle the sub-pack's archives separately.
Everything loads and validates, but assets behave wrongly in world Not a packaging problem; see moving a whole set. Structural checks cannot detect content authored for a different game build, vehicle set or load order. Test each pack in game.
Mods present on first join, gone after a disconnect and reconnect 🟡 Mods mount once, at game launch (OnInitialMount). On disconnect, CleanupStreaming removes every mod_/faux_pack streaming tag and there is no re-mount hook, so a reconnect without restarting FiveM can come back with the mods gone, caret models and map packs alike. Derived from the loader source; if a pack seems to have "fallen off" mid-session, restart FiveM rather than re-wrapping it.
Minimap tiles load but the pause map will not zoom out fully Something is overriding common/data/ui/mapzoomdata.meta. A pack whose own minimap is a single untiled image zeroes vTiles on ZOOM_LEVEL_24, which stops a tiled minimap from ever being tiled. Two minimap packs cannot share a client.

34. Etiquette and scope

Two things worth being straight about, because they decide whether this is worth doing at all.

Client-side means client-side. Nothing here is transmitted. A tattoo pack you wrap changes what your game draws on your screen; every other player sees the vanilla asset, or whatever the server streams. That is the same deal as the RAGEMP dlcpacks folder, and it is the whole point for a screenshot or roleplay-appearance workflow. It is not a way to add content to a server.

The server decides. sv_pureLevel exists, servers use it, and the Cfx position on client-side mods is that they are unsupported and at your own risk. A pack that loads on one server will hard-fail your join on another. Keep the mods/ folder something you can empty quickly.