> For the complete documentation index, see [llms.txt](https://sebun1.gitbook.io/skins/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://sebun1.gitbook.io/skins/schema/skin-group.md).

# Skin Group

## Overview

Each Skin Group groups together a set of Material Targets. Whenever a gear has a particular Skin Group select, the mod will go through all the gear parts of that gear and try to match the material of each part to some material target inside the Skin Group. If a Material Target is matched with a gear part, the definitions inside it will be applied to that gear part. With a complete set of Material Targets defined within a Skin Group, we can successfully place a custom skin onto a specific gear.

Given that, it is sensible to place Material Targets definitions for multiple gears of the same style inside the same Skin Group. If the some of the gears happen to share a gear part, their replacement for that gear part on those gears will be the same version. This is both a limitation and a feature: if you have a set of skins of the same style, it is probably the case that the texture to apply on the shared gear parts across gears is the same. However, if you want different gears who share a gear part to have a different texture on that gear part, you cannot define them in the same Skin Group.

For a easy way to create these manifest files, check out [Creating Skin Groups](/skins/editor/creating-skin-groups.md).

## Folder Structure / Skin Folders

Each Skin Group lives in its own subdirectory in a Skins Folder. The mod looks for Skin Groups (i.e. the Skin Group's own folder) in the [global and per-profile skins folders](/skins/schema/paths.md).

The mod will first load all Skin Groups inside the global skins folder, then look at the per-profile skins folder which the current game instance is launched with. If duplicates exist, the version in the global skins folder takes precedence since it is loaded first. If the "[move per-profile to global](/skins/plugin/plugin-config.md#id-1-general)" setting is enabled (**default behavior**), the per-profile duplicate version will replace the global version inside the global skins folder before any load happens since it is considered more update to date (without this mechanic, updates to skins will not work).

***

Inside one of those root folders, create a subdirectory for your skin. The folder can be named anything, but the mod normalizes folder names to match the group's GUID on startup (see [Folder Normalization](#folder-normalization)).

Inside the folder, place a manifest file named exactly `manifest.skingroup.json`. Any number of Material Target files (`.mattarget.json`) can be placed anywhere within the folder tree — they are discovered recursively.

### Layout

```
skins/                            ← this is the skins folder (per profile or global)
  my-awesome-shotgun-skin/        ← folder name (will be renamed to GUID on startup)
    manifest.skingroup.json       ← skin group manifest (required, must be on this folder level)
    player_main.mattarget.json    ← material target (can be any name)
    textures/
      albedo.png
      normal.png
```

### Folder Normalization

On startup the mod renames each skin subdirectory to its GUID so that folder names are always unique across different authors. If two folders would map to the same GUID, only the first one is renamed and a warning is logged — you should treat this as a duplicate GUID error and change one of the GUIDs.

This behaviour can be disabled with the **Do Not Normalize Skin Folder Names** config option, though doing so is not recommended in production as it can hide GUID collisions.

## Manifest Schema

`manifest.skingroup.json`

The manifest file **MUST** be the above name. No other name is recognized.

```jsonc
{
  "guid": "your-name.skin-name",
  "name": "My Awesome Skin",
  "author": "YourName",
  "desc": "Optional description.",
  "filterGroups": [ /* see Filters section */ ]
}
```

### Fields

<table><thead><tr><th width="142.833251953125">Field</th><th width="176">Type</th><th width="107.1666259765625" data-type="checkbox">Required</th><th>Description</th></tr></thead><tbody><tr><td><code>guid</code></td><td>string</td><td>true</td><td>Unique identifier across all skin groups. <strong>Never change this after publishing</strong> — it is how the mod identifies which skin a player has selected.</td></tr><tr><td><code>name</code></td><td>string or locale map</td><td>true</td><td>Display name of the skin group.</td></tr><tr><td><code>author</code></td><td>string or locale map</td><td>true</td><td>Author name, shown in the skin selector UI.</td></tr><tr><td><code>desc</code></td><td>string or locale map</td><td>false</td><td>Optional description.</td></tr><tr><td><code>filterGroups</code></td><td>array of filter group objects</td><td>true</td><td>Controls which gears this skin group can be applied to. An empty array means the group <strong>never</strong> matches. See Filters below.</td></tr></tbody></table>

### Localized Strings

`name`, `author`, and `desc` accept either a plain string or a language map. When a plain string is used, it is shown regardless of the player's language setting. When a language map is used, the **first entry** in the map acts as the fallback for any language not explicitly listed — so put your primary language first.

Keys are BCP 47 locale codes:

<table><thead><tr><th width="168.8333740234375">Key</th><th width="225.1666259765625">Language</th></tr></thead><tbody><tr><td><code>en</code></td><td>English</td></tr><tr><td><code>fr</code></td><td>Français</td></tr><tr><td><code>de</code></td><td>Deutsch</td></tr><tr><td><code>es</code></td><td>Español</td></tr><tr><td><code>it</code></td><td>Italiano</td></tr><tr><td><code>ja</code></td><td>日本語</td></tr><tr><td><code>ko</code></td><td>한국어</td></tr><tr><td><code>pt-BR</code></td><td>Português (Brasil)</td></tr><tr><td><code>ru</code></td><td>Русский</td></tr><tr><td><code>pl</code></td><td>Polski</td></tr><tr><td><code>zh-Hans</code></td><td>简体中文</td></tr><tr><td><code>zh-Hant</code></td><td>繁體中文</td></tr></tbody></table>

```jsonc
// Plain string
"name": "My Skin"

// Language map — first entry is the fallback
"name": {
  "en": "My Skin",
  "fr": "Mon skin",
  "zh-Hans": "我的皮肤"
}
```

### Filters

The filter system controls which gears a skin group can be assigned to. The structure is a [**Disjunctive Normal Form (DNF)**](https://en.wikipedia.org/wiki/Disjunctive_normal_form): `filterGroups` is a list of clauses evaluated with **OR** semantics, and each clause contains a list of conditions evaluated with **AND** semantics.

In layman terms: the group matches a gear if **any** clause matches, and a clause matches if **all** of its conditions match.

```jsonc
"filterGroups": [
  {
    "filters": [
      { "type": "OfflineGearPersistentID", "value": "128" },
      { "type": "RundownName",             "value": "xiaotian-Survival" }
    ]
  },
  {
    "filters": [
      { "type": "OfflineGearPersistentID", "value": "256" }
    ]
  }
]
```

The above allows the skin on gear `128` *only when* rundown `xiaotian-Survival` is loaded, **or** on gear `256` in any rundown.

#### Filter Object Fields

<table><thead><tr><th width="109.5">Field</th><th width="123.6666259765625">Type</th><th width="238.6666259765625">Default</th><th>Description</th></tr></thead><tbody><tr><td><code>type</code></td><td>string</td><td><code>OfflineGearPersistentID</code></td><td>The filter type — see types below.</td></tr><tr><td><code>value</code></td><td>string</td><td><code>""</code></td><td>The value to match against; interpretation depends on <code>type</code>.</td></tr><tr><td><code>invert</code></td><td>boolean</td><td><code>false</code></td><td>When <code>true</code>, the result of the match is negated (NOT).</td></tr></tbody></table>

#### Filter Types

<table><thead><tr><th width="239">Type</th><th>Matches when...</th></tr></thead><tbody><tr><td><code>OfflineGearPersistentID</code></td><td>The gear's offline persistent ID equals <code>value</code> (parsed as an integer). Use the SkinGroup Editor, the gear dump, or datablocks to look up IDs.</td></tr><tr><td><code>RundownName</code></td><td>The currently loaded MTFO rundown's folder name equals <code>value</code>. Set to <code>__vanilla__</code> to match when no custom rundown is loaded.</td></tr></tbody></table>

{% hint style="info" %}
I plan to add additional filters in the future (e.g. currently, I am thinking of adding material matching for groups as well)
{% endhint %}

## Full Example

from the Tron skin pack

```jsonc
{
  "name": {
    "en": "Tron",
    "zh-Hans": "创"
  },
  "desc": {
    "en": "Series of tron-themed skins",
    "zh-Hans": "一系列基于《创》风格制作的皮肤"
  },
  "author": {
    "en": "food",
    "zh-Hans": "food"
  },
  "guid": "io.takina.gtfo.skins.Tron",
  "filterGroups": [
    {
      "filters": [
        {
          "type": "OfflineGearPersistentID",
          "value": "18"
        }
      ]
    },
    {
      "filters": [
        {
          "type": "OfflineGearPersistentID",
          "value": "39"
        }
      ]
    },
    {
      "filters": [
        {
          "type": "OfflineGearPersistentID",
          "value": "43"
        }
      ]
    },
    {
      "filters": [
        {
          "type": "OfflineGearPersistentID",
          "value": "42"
        }
      ]
    },
    {
      "filters": [
        {
          "type": "OfflineGearPersistentID",
          "value": "53"
        }
      ]
    }
  ]
}
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://sebun1.gitbook.io/skins/schema/skin-group.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
