Skip to content
Back to skills

Dayz Particles

ASecurity

Use when: particles, .ptc, .emat, ParticleSource, SEffectManager, PlayOnObject, cfgeffectarea, smoke/sparks without Workbench, sandstorm, ceiling dust, ParticleParamsOverrideData, VehicleVFX, IsHeadlessOrDedicatedServer. Client-only .ptc/.emat. Not sound: dayz-sound-system.

  • 16 stars
  • 0 votes
  • 2 copies
  • 2 views
  • Added October 2, 2026
ai-agentspythongoc++springtestingapi

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 9 files and shows the line behind each finding

Scanned October 2, 2026

npx -y skills add willy92wins/DayZ-Modding-Knowledge-Pack --skill dayz-particles --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Dayz Particles?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Dayz Particles
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/willy92wins-dayz-particles/badge)](https://www.skillsdirectory.com/skills/willy92wins-dayz-particles)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: dayz-particles
description: "Use when: particles, .ptc, .emat, ParticleSource, SEffectManager, PlayOnObject, cfgeffectarea, smoke/sparks without Workbench, sandstorm, ceiling dust, ParticleParamsOverrideData, VehicleVFX, IsHeadlessOrDedicatedServer. Client-only .ptc/.emat. Not sound: dayz-sound-system."
---

# DayZ Particle Effects — Complete Modding Reference

## CRITICAL: .ptc AND .emat ARE PLAIN TEXT

DayZ particle files (.ptc) and material files (.emat) are plain text configs,
not binary. Custom particles can be created programmatically without Workbench.

| Layer | Format | Creatable? | Purpose |
|---|---|---|---|
| `.ptc` | Plain text config | YES | Particle effect (emitters, physics, curves) |
| `.emat` | Plain text config | YES | Material (texture ref, blend mode, color) |
| `.edds` | Standard DDS renamed | YES | Billboard sprite texture |
| `.meta` | Plain text (GUIDs) | Workbench only | NOT needed for mod runtime |

All particles are CLIENT-ONLY. Server controls state via SyncVars; client
creates/destroys particles in response.
(until 1.29: this was the documented PlayOnObject contract; wrecks gated with `!g_Game.IsDedicatedServer()`. `SEffectManager.CreateParticleServer` already existed for trees/fishing/traps.) (since 1.30 Exp: local PlayOnObject is still client-only, but ceiling dust and other synced effecters spawn on the server via `CreateParticleServer`; vanilla wrecks, FireplaceBase, and `ParticleManager.GetInstance()` now gate with `!g_Game.IsHeadlessOrDedicatedServer()`. Details: `references/dayz-1-30-particles.md`.)

## EFFECT-AREA PREFLIGHT (added 2026-08-31)

For `cfgeffectarea.json`, run **Effect areas: emitter budget and vertical band** plus its correction
below **before** choosing a radius. Compute the exact per-ring upper budget, keep it below 1,000 with
margin, measure the full elevation/underground band, and compare every global `SafePositions` entry
against every area.

---

## .ptc FILE FORMAT (verified from 9 vanilla files)

One EffectDef with one or more EmitorDef blocks. Each emitter is an independent
particle stream. Simplest particle (debug_dot): 1 emitter, 478 bytes. Complex
fire (fire_small_camp_01): 6 emitters (glow + flames + burst + short + sparks + smoke).

```
EffectDef {
 Emitors {
  EmitorDef <name> {
   ShapeType Point
   MaxNum 10
   BirthRate 5
   Material "{GUID}path/to/material.emat"
   SizeMultiplier 0.5
   Velocity 2
   LifeTime 1.5
   Color {
    0 1 0.8 0.3 1 0.7 0.5 0.25
   }
   Alpha {
    0 0 0.2 1 1 0
   }
   Repeat 1
  }
 }
}
```

### Property categories (56 total — full list in references/ptc-format-reference.md)

**Shape & emission:** ShapeType (Point/Box/Ellipse), ShapeSize (x y z),
ConeAngle (yaw pitch roll deg), MaxNum, BirthRate, BirthRateRND, Offset (x y z)

**Visual:** Material ("{GUID}path.emat"), SizeMultiplier, SizeRND,
BillboardingType (Full), CenterY (-1=bottom, 0=center), StretchMultiplier

**Motion:** Velocity, VelocityRND, AirResistance, AirResistanceRND,
GravityMultiply, GravityMultiplyRND, Wind (0-1), Spring, Restitution,
VelAngle (0/1), VelAffect (0-1)
(since 1.30 Exp: after play, `EffectParticle.ParticleParamsOverride(ParticleParamsOverrideData)` can retune `EmitorParam.LIFETIME` / `VELOCITY` / `SIZE` / `AIR_RESISTANCE` from script — used by vehicle dust and wheel contact. See `references/dayz-1-30-particles.md`.)

**Lifetime:** LifeTime (seconds), LifeTimeRND, EffectTime (cycle duration),
Repeat (0=one-shot auto-deletes, 1=loop MUST stop manually)

**Animation:** AnimOnce, AnimFPS, LifetimeByAnim, RandomFrame

**Rotation:** RandomRotDir, RotMultiplier, RandomAngle

**Curves (keyframe pairs: time value time value ...):**
Color { t r g b t r g b }, Alpha { t a t a },
Size { t s t s }, RotationSpeed { t s t s }

**Master curves (modulate over EffectTime):**
ColorMast, AlphaMast, SizeMast, BRateMast, VelocityMast, AirResistanceMast

---

## .emat MATERIAL FORMAT (verified from 8 vanilla materials)

### Shader: `Particle` (fire, sparks, water, most particles)
```
Particle {
 AlbedoMap "{GUID}Graphics/Particles/sprites/texture.edds"
 Color 0 0 0 1
 Emissive 1 1 1 0
 BlendMode Additive_AlphaModulate
 Sort translucent
 Softness 2
 TileU 4
 TileV 2
}
```

(until 1.29: particle `.emat` shaders documented here are `Particle` and `ParticleSprite`.) (since 1.30 Exp: weather VFX also ship postprocess `.emat` that are **not** particle materials — sandstorm uses shader `SnowEffect` in `exp\graphics\graphics\Materials\postprocess\sandstorm.emat:1`. Do not treat that file as a `.ptc` material.)

### Shader: `ParticleSprite` (steam, flares, glow billboards)
```
ParticleSprite {
 AlbedoMap "{GUID}Graphics/Particles/sprites/texture.edds"
 Color 0.95 0.95 0.95 1
 Diffuse 0.95 0.95 0.95 1
 Ambient 0.59 0.59 0.59 1
 Softness 2
}
```

### All .emat properties (12):
AlbedoMap (texture GUID+path), Color (RGBA 0-1), Diffuse (RGBA),
Ambient (RGBA), Emissive (RGBA, self-glow), BlendMode (Additive_AlphaModulate
or numeric), Sort (translucent/overlay/0), Softness (0-2, edge blending),
TileU/TileV (sprite sheet cols/rows), CameraBlendFar (distance fade),
BidirLightScale (bidirectional lighting)

Full reference with examples: `references/emat-format-reference.md`

---

## GUID REFERENCE SYSTEM

Resources reference each other via `{HEXGUID}relative/path.ext`.

- **Vanilla→vanilla**: GUIDs embedded, engine resolves by GUID first
- **Mod .ptc→vanilla .emat**: USE vanilla GUID+path (mandatory, verified)
- **Mod .emat→vanilla .edds**: USE vanilla GUID+path (safe)
- **Mod .ptc→mod .emat**: path-only may work (needs in-game testing)
- **.meta files NOT needed** for mod runtime loading

---

## VANILLA MATERIAL LIBRARY (11 GUIDs mapped)

Reference these in custom .ptc to reuse vanilla visuals:

| GUID | Short name | Visual type |
|---|---|---|
| `{CB7AF4FD8ACBFDFC}` | glow/glow1.emat | Soft glow dot |
| `{009C2EBAACD2D72A}` | fire/sparks1.emat | Bright spark point |
| `{0B829A150C6A13E5}` | fire/sparks_06.emat | Spark strip (8-tile) |
| `{853257EDA4B1E35D}` | fire/fire_flame_01.emat | Fire flame billboard |
| `{3E6CE4D61F8AC71F}` | fire/fire_medium_camp_02.emat | Medium fire anim |
| `{83BCB6516091DB01}` | fire/fire_medium_camp_04.emat | Fire burst anim |
| `{62F13B8B540EBC80}` | fire/fireball_1.emat | Fireball |
| `{00BEA27443975BEB}` | smoke/smoke_anim_02.emat | Animated smoke puff |
| `{008E25684854DAB8}` | smoke/steam_small_cook_01.emat | Steam/vapor cloud |
| `{8FE2383D6913E098}` | enviroment/water_splash_01_NoEm.emat | Water splash |
| `{D29B53088A5A3911}` | smoke/smoke_dust_01_W.emat | White dust/mist |

All paths prefixed with `Graphics/Particles/materials/`.

---

## THREE SCRIPT API LAYERS

### 1. Legacy `Particle` (scripts/3_Game/Particles/Particle.c)
Creates `#particlesourceenf` child entity. EOnFrame lifetime tracking.
Auto-deletes non-looping particles. Used by FireplaceBase, torches, cooking.

### 2. `ParticleSource` extends Particle (ParticleSource.c)
Native C++ backed. No EOnFrame. Auto-destroy flags: ON_END, ON_STOP, ALL, NONE.
StopParticleFlags: `NONE = 0` (no-op; default `StopParticle()` already does gradual fade), IMMEDIATE, PAUSE (freeze visible).

### 3. `ParticleManager` pool (ParticleManager.c)
Global instance via `ParticleManager.GetInstance()` — 10000 pool, client only,
returns null on dedicated server. Pre-allocates+reuses ParticleSource objects.
(until 1.29: GetInstance skipped create when `IsDedicatedServer()`.) (since 1.30 Exp: skipped when `IsHeadlessOrDedicatedServer()` — headless clients also get null. `exp\scripts\scripts\3_Game\Particles\ParticleManager\ParticleManager.c:65`.)
Mods can also `new ParticleManager(settings)` to instantiate their own pools
(the test framework does this); GetInstance() is the global, not a hard singleton.

### Wrapper: `SEffectManager`
PowerGenerator pattern: `SEffectManager.PlayOnObject(effect, parent, pos, ori)`.
Cleanup: `SEffectManager.DestroyEffect(m_Smoke)`.
(since 1.30 Exp: synced spawn uses `SEffectManager.CreateParticleServer(pos, EffecterParameters)` — ceiling dust: `DustEffectCeilingHandlerSynced`. `exp\scripts\scripts\3_Game\EffectManager.c:586`.)

Full API signatures: `references/script-api-reference.md`

---

## API QUICK REFERENCE

```
// Static create+play
Particle p = Particle.PlayOnObject(ID, parent, localPos, localOri, forceWorldRot);
Particle p = Particle.PlayInWorld(ID, worldPos);
// ParticleManager.GetInstance() returns null on dedicated server — always guard.
// (since 1.30 Exp: also null on headless — use !g_Game.IsHeadlessOrDedicatedServer().)
ParticleManager pm = ParticleManager.GetInstance();
ParticleSource ps;
if (pm) ps = pm.PlayOnObject(ID, parent, localPos);

// Instance control
p.PlayParticle(optionalNewId);
p.StopParticle(flags);  // StopParticleFlags: NONE, IMMEDIATE, PAUSE
p.Stop();               // legacy alias
p.IsParticlePlaying();
p.ResetParticle();      // ParticleSource only
p.RestartParticle();    // reset+play
p.DisableAutoDestroy(); // ParticleSource only

// Parameter tuning (-1 = all emitters)
p.SetParameter(emitter, EmitorParam.AIR_RESISTANCE, 3.0);
p.ScaleParticleParamFromOriginal(EmitorParam.SIZE, 0.5);

// Wiggle
p.SetWiggle(randomAngle, randomInterval);
p.StopWiggle();
```

---

## REGISTERING CUSTOM PARTICLES

### Mod file structure
```
LFPowerGrid/
  data/particles/
    lfpg_sprinkler_spray.ptc
    lfpg_overload_sparks.ptc
  data/particles/materials/         ← optional custom materials
    lfpg_blue_water.emat
  scripts/3_Game/
    LFPG_ParticleList.c
```

### Registration (3_Game layer)
```
modded class ParticleList
{
    static const int LFPG_SPRINKLER_SPRAY = RegisterParticle(
        "LFPowerGrid/data/particles/", "lfpg_sprinkler_spray");
    static const int LFPG_OVERLOAD_SPARKS = RegisterParticle(
        "LFPowerGrid/data/particles/", "lfpg_overload_sparks");
};
```

### AddonBuilder filter (include .ptc .emat .edds)
```
*.emat;*.edds;*.meta;*.ptc;*.c;*.imageset;*.layout;*.ogg;*.json;*.xml;*.paa;*.rvmat
```

---

## LFPG INTEGRATION PATTERNS (summary)

Full code: `references/lfpg-integration-patterns.md`

**Pattern A** — FireplaceBase helpers: `LFPG_PlayParticle(out Particle, id, pos)` /
`LFPG_StopParticle(out Particle)` with null-safety and server guard.

**Pattern B** — SyncVar toggle: server sets state→SetSynchDirty, client
creates/destroys in LFPG_OnVarSync.

**Pattern C** — One-shot sparks: non-looping (Repeat 0) particles auto-delete,
no cleanup needed. Use for overload events.

**Pattern D** — SEffectManager wrapper: for combining particle+sound. Cleanup
ONLY via SEffectManager.DestroyEffect().

**Pattern E** — Runtime parameter tuning: SetParameter for AIR_RESISTANCE,
ScaleParticleParamFromOriginal for SIZE.
(since 1.30 Exp: vehicle VFX use `ParticleParamsOverride(ParticleParamsOverrideData)` instead of ad-hoc SetParameter after Play.)

**Cleanup contract**: EVERY device with looping particles MUST clean up in
EEDelete + LFPG_OnWiresCut + destructor.

---

## PITFALLS AND HARD RULES

1. **Particles are CLIENT-ONLY.** Wrap in `#ifndef SERVER`.
   (until 1.29: PlayOnObject gated with `!IsDedicatedServer()`.) (since 1.30 Exp: vanilla uses `!g_Game.IsHeadlessOrDedicatedServer()` so headless clients also skip VFX. Server-spawned effecters use `CreateParticleServer`, not PlayOnObject.)
2. **Looping particles (Repeat 1) NEVER end.** MUST Stop() + null ref.
3. **Stop() is gradual.** Use StopParticleFlags.IMMEDIATE for instant removal.
4. **ObjectDelete(particle)** valid for ParticleSource with DisableAutoDestroy().
5. **Render distance ~200m.** Engine limit, no workaround.
6. **Duplicate filenames** across mods: use unique prefixes (`lfpg_`).
7. **ParticleManager.GetInstance()** returns null on server. Always null-check.
   (since 1.30 Exp: also null on headless clients — `IsHeadlessOrDedicatedServer()`.)
   Guards 1 and 7 are complementary, not redundant: `#ifndef SERVER` (pitfall 1) is
   compile-time exclusion for whole client-only blocks; the null-check (this pitfall)
   is runtime safety for code compiled on both sides (a diag/SP client-server build has
   no SERVER define but still runs server-side). Side checks: `enforce-script-reference`
   hard rules 19-20.
8. **SEffectManager.DestroyEffect()** is the ONLY safe Effect cleanup.
9. **AIR_RESISTANCE** controls smoke rise. FireplaceBase adjusts for ceilings.
10. **force_world_rotation=true** for smoke/steam that should always rise up.
11. **.meta files NOT needed** for mod runtime.
12. **GUIDs mandatory** when referencing vanilla .emat/.edds from mod .ptc.
13. **.ptc is PLAIN TEXT.** Can be generated by Python scripts, no Workbench needed.
14. **EffectTime + Repeat** interact: EffectTime is the master curve cycle, Repeat
    loops the entire effect. One-shot (Repeat 0) plays once for EffectTime duration.

---

## VANILLA PARTICLE CATALOG — KEY CATEGORIES

Full catalog: `references/vanilla-particle-catalog.md` (276 entries; version-dependent).
(since 1.30 Exp: new constants include tandoor fireplace, desert/mud-brick impacts, ceiling dust, vehicle dust, oil-pit fire, sand grenade explosions — listed in that catalog's 1.30 section.)

### Electricity
`POWER_GENERATOR_SMOKE` (1 emitter, loop), `BARBED_WIRE_SPARKS` (1 emitter, one-shot)

### Water
`WATER_JET` (2 emitters, loop), `WATER_JET_WEAK` (2 emitters, loop),
`WATER_SPILLING`, `DROWNING_BUBBLES`

### Fire
`CAMP_SMALL_FIRE` (6 emitters), `CAMP_STOVE_FIRE`, `BONFIRE_FIRE`

### Smoke/Steam
`CAMP_SMALL_SMOKE` (1 emitter), `CAMP_STEAM_2END`, `EVAPORATION`, `SPOOKY_MIST`

### Environment
`ENV_SWARMING_FLIES`, `SMOKING_HELI_WRECK`, `HOTPSRING_WATERVAPOR`,
`GEYSER_NORMAL`, `GEYSER_STRONG`, `VOLCANO`
(since 1.30 Exp: `OIL_FIRE1`/`OIL_FIRE2`, `DUST_GROUND`/`DUST_CLOUD`/`DUST_VEHICLE`, `CEILING_WOOD`. Weather sandstorm also references `Graphics/Particles/sandstorm/sandstorm`.)

---

## REFERENCE FILES

- `references/standalone-particle-reference.md` — standalone predecessor reference: multi-emitter composition (PLANES_EX/IMPACT/BOOM), animated + Mast curves, CfgCloudlets conversion. [merged 2026-06-05]

- `references/vanilla-particle-catalog.md` — 276 vanilla particles by category (version-dependent)
- `references/lfpg-integration-patterns.md` — 5 LFPG code patterns
- `references/ptc-format-reference.md` — 56-property .ptc reference with vanilla examples
- `references/emat-format-reference.md` — .emat format, both shader types, all 12 properties
- `references/script-api-reference.md` — Particle/ParticleSource/ParticleManager signatures
- `references/answeroverflow-2026-05-17.md` — community snippets verified vs vanilla (PlayOnObject pattern with client guard)
- `references/dayz-1-30-particles.md` — DayZ 1.30 Exp: dust, VehicleVFX, ParticleParamsOverrideData, sandstorm, oil pits, SurfaceInfo IDs, migration

## Effect areas (`cfgeffectarea.json`): emitter budget and vertical band (added 2026-08-31)

Run these gates **before** choosing a large contaminated-area radius.

### Hard particle budget

Each effect area has a hard cap of 1,000 emitters: `const int PARTICLES_MAX = 1000`
(`VANILLA/scripts/4_world/classes/contaminatedarea/effectarea.c:84`), enforced while
`SpawnParticles` fills the area (`:421`). Exceeding it does not shrink the damage radius. The area
can remain lethal beyond the visible particle core and logs
`Not enough particles in pool for EffectArea` (`:440`). A valid JSON and a booting server do not
prove that the perimeter is visible.

For the static `FillWithParticles` route, budget first with:

```text
N = ceil((Radius + OuterOffset - InnerPartDist/2) / InnerPartDist)
emitters ~= 6.283 * N^2 * (VerticalLayers + 1)
```

Vanilla computes `circumference = 2 * Math.PI2 * ringRadius` (`effectarea.c:389`) while
`Math.PI2` is already 6.28318530717958 (`VANILLA/scripts/1_core/proto/enmath.c:13`). That quirk
uses approximately `4*pi*r`, twice the ordinary circumference. Budgeting with `2*pi*r` therefore
under-counts. Increasing `InnerPartDist` lowers the ring count `N`; it does not reduce the emitter
count within an existing ring.

For static areas, only `InnerPartDist` and `OuterOffset` feed this route
(`contaminatedarea.c:70`). `InnerRingCount`, `OuterPartDist`, and `OuterRingToggle` belong to
`PlaceParticles` and are inert here. Do not tune them to solve a static-area density problem.

### Damage and visibility are vertically bounded

The trigger is a finite cylinder, built by
`SetCollisionCylinderTwoWay(radius, -(NegHeight + c), PosHeight - c)` with
`c = (PosHeight - NegHeight) * 0.5` (`effectarea.c:495-497`). Particle placement is culled against
the same band after snapping to terrain (`:413,421`). Choose `PosHeight` and `NegHeight` from the
actual elevation range inside the radius, not from a small vanilla zone. Underground spaces also
count: particles snap to `SurfaceY` and are never spawned below terrain, so a bunker can be lethal
inside the cylinder while showing no gas. That can be deliberate, but it must not be accidental.
(until 1.29: `SpawnParticles` wrote `partPos[1] = g_Game.SurfaceY(...)`.) (since 1.30 Exp: it prefers `g_Game.SurfaceRoadY` when the particle is above the road surface — bridges/roads. `exp\scripts\scripts\4_World\Classes\ContaminatedArea\EffectArea.c:414-417`. Tick hook is `OnCEIterate`, not `OnCEUpdate`.)

### Safe positions and schema

`SafePositions` is global, not attached to one area. `GetClosestSafePos` searches the whole array in
2D (`VANILLA/scripts/4_world/static/miscgameplayfunctions.c:1698,1717`) during the early-presence
rescue path (`:1692`; caller `areaexposure.c:44`). After any radius change, measure every safe
position against every area; a rescue point inside another cylinder can teleport a reconnecting
player from gas to gas.

Treat `JsonDataContaminatedArea` as the schema authority
(`VANILLA/scripts/4_world/classes/contaminatedarea/jsondatacontaminatedarea.c:17-34`). Community
fields named `InnerRingRatio` and `OuterRingRatio` are not in that schema. Large-radius height values
remain `[VERIFY IN GAME]` until the real terrain and underground depth are measured.

## Correction: effect-area budget and pool diagnostics (added 2026-08-31)

This section supersedes the approximate `6.283 * N^2` budget and the RPT claim above. For the normal
large-area branch (`R >= 1.25 * s`), use the same rounded ring spacing and per-ring floors as
`FillWithParticles`:

```text
R = Radius + OuterOffset
s = max(InnerPartDist, 1)
N = ceil((R - s/2) / s)
d = (R - s/2) / N
XZ = 1 + sum(floor(2 * PI2 * d * k / s), k = 1..N)
budget_max = XZ * (VerticalLayers + 1)
```

`PI2` is `2*pi`, so the source's `2 * PI2 * ringRadius` remains the intentional doubled
circumference. The center emitter is the leading `1`. Small areas take separate one-ring/center
branches in `effectarea.c:360-373`; use those branches directly rather than this large-area formula.
As a check, `Radius=500`, `OuterOffset=20`, `InnerPartDist=70`, and `VerticalLayers=1` give
`N=7`, `d=69.286`, `XZ=346`, and `budget_max=692`.

Reaching `PARTICLES_MAX` truncates additional visual emitters in `SpawnParticles`
(`effectarea.c:411-426`) but does **not** itself log `Not enough particles in pool`. That error is in
`InsertParticles` and means `ParticleManager` returned fewer objects than the count requested
(`:429-447`). Do not use the RPT message as the cap gate. Vertical culling can reduce the actual
count, but an approximation that under-counts cannot authorize a radius; calculate the upper budget
and confirm the visible perimeter in game.

## DayZ 1.30 Exp (build 1.30.164014)

Full detail: `references/dayz-1-30-particles.md`. Surface-dust / ceiling-dust / VehicleVFX / sandstorm `.ptc` path live there.

### What changes
- Client VFX gate is `!g_Game.IsHeadlessOrDedicatedServer()` (wrecks, FireplaceBase, ParticleManager).
- Synced ceiling dust: `SEffectManager.CreateParticleServer` + `ParticleEffecterParameters("CeilingDustEffecter", ...)`.
- Runtime retune: `ParticleParamsOverrideData.m_FloatModifiers` via `VehicleVFXComponent`.
- Surface particle IDs move to `SurfaceInfo.GetStepParticleId` / `GetWheelParticleId` / `GetVehicleDustParticleId` / `GetCeilingDustParticleId` (the `Surface.GetStepsParticleID` / `GetWheelParticleID` wrappers are `[Obsolete]`).
- Effect areas: `OnCEIterate` replaces `OnCEUpdate`; `SpawnParticles` prefers `SurfaceRoadY`.
- New ParticleList constants: tandoor, desert/mud-brick impacts, `CEILING_WOOD`, dust, `OIL_FIRE1`/`OIL_FIRE2`. Weather sandstorm is a `SnowEffect` postprocess `.emat` plus `particlePath = "Graphics/Particles/sandstorm/sandstorm"`.

### What breaks (1.29 mods)
| Symptom | Severity | Change |
|---|---|---|
| Particles play on headless client | MEDIUM | Replace `!IsDedicatedServer()` with `!IsHeadlessOrDedicatedServer()`. |
| Wheel/step particles ignore custom surfaces | MEDIUM | Stop calling `Surface.GetStepsParticleID` / `GetWheelParticleID`; use `SurfaceInfo`. |
| `EffWheelSmoke.SetSurface(string)` | LOW | Use `SelectFromSurface(SurfaceInfo)`. |
| Custom `EffectArea` never ticks / particles float above bridges | MEDIUM | Override `OnCEIterate`; snap with `SurfaceRoadY`. |

### 1.30 migration checklist
- [ ] Search the mod for `IsDedicatedServer()` on particle spawn; switch VFX to `IsHeadlessOrDedicatedServer()`.
- [ ] Replace `Surface.GetStepsParticleID` / `GetWheelParticleID` with `SurfaceInfo.GetByName(...).GetStepParticleId()` / `GetWheelParticleId()`.
- [ ] Replace `EffWheelSmoke.SetSurface` with `SelectFromSurface`.
- [ ] Custom EffectArea: `OnCEIterate(float currentTime, float elapsedTime)` and `SurfaceRoadY`.
- [ ] Vehicle dust/exhaust: register through `VehicleVFXComponent` and override `ParticleParamsOverride`.
- [ ] Do not copy `sandstorm.emat` as a particle material — shader is `SnowEffect`.

### 1.30 references in this skill
- [DayZ 1.30 particles](references/dayz-1-30-particles.md)
- [Script API 1.30](references/script-api-reference.md#dayz-130-exp-surfaceinfo-vehicle-vfx-synced-spawn)
- [Vanilla catalog 1.30 constants](references/vanilla-particle-catalog.md#dayz-130-exp-new-particlelist-constants)
- [PlayOnObject client gate](references/answeroverflow-2026-05-17.md)

Files in this skill

  • SKILL.md21.6 KB
  • references/answeroverflow-2026-05-17.md3 KB
  • references/dayz-1-30-particles.md9.4 KB
  • references/emat-format-reference.md6.6 KB
  • references/lfpg-integration-patterns.md8.5 KB
  • references/ptc-format-reference.md9.6 KB
  • references/script-api-reference.md11.9 KB
  • references/standalone-particle-reference.md14.7 KB
  • references/vanilla-particle-catalog.md20.6 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.