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.

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.

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” 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).

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.

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

Fields

FieldTypeRequiredDescription
guidstring✓Unique identifier across all skin groups. Never change this after publishing — it is how the mod identifies which skin a player has selected.
namestring or locale map✓Display name of the skin group.
authorstring or locale map✓Author name, shown in the skin selector UI.
descstring or locale map—Optional description.
filterGroupsarray of filter group objects✓Controls which gears this skin group can be applied to. An empty array means the group never matches. See Filters below.

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:

KeyLanguage
enEnglish
frFrançais
deDeutsch
esEspañol
itItaliano
ja日本語
ko한국어
pt-BRPortuguês (Brasil)
ruРусский
plPolski
zh-Hans简体中文
zh-Hant繁體中文
// 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): 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.

"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

FieldTypeDefaultDescription
typestringOfflineGearPersistentIDThe filter type — see types below.
valuestring""The value to match against; interpretation depends on type.
invertbooleanfalseWhen true, the result of the match is negated (NOT).

Filter Types

TypeMatches when…
OfflineGearPersistentIDThe gear’s offline persistent ID equals value (parsed as an integer). Use the SkinGroup Editor, the gear dump, or datablocks to look up IDs.
RundownNameThe currently loaded MTFO rundown’s folder name equals value. Set to __vanilla__ to match when no custom rundown is loaded.

Info

I plan to add additional filters in the future (e.g. currently, I am thinking of adding material matching for groups as well)

Full Example

from the Tron skin pack

{
  "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"
        }
      ]
    }
  ]
}